Compare commits
439 Commits
64a0fdf81d
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| ec287f087c | |||
| 758696f7c9 | |||
| de9cd618b6 | |||
| e7fb01be9f | |||
| e4a34a7dad | |||
| d799c3eb1f | |||
| c5fabf0e07 | |||
| 3cdf9eb185 | |||
| 6efccae030 | |||
| 93cbbea3c7 | |||
| cace8640e9 | |||
| a3ce98a984 | |||
| 4f3e3a28cc | |||
| 657ffaae11 | |||
| 700d5a63a7 | |||
| 9531ac2e23 | |||
| 288da412d9 | |||
| 1b34520a66 | |||
| 23b285cf49 | |||
| 27707b120c | |||
| 14978087e3 | |||
| 3021788271 | |||
| 358bf0918e | |||
| 656784af00 | |||
| c8b71f290b | |||
| 8f109bf92f | |||
| 268b380867 | |||
| 71889ddfac | |||
| 0a74ff8770 | |||
| f158181ccf | |||
| e585abc378 | |||
| ccf16b6abc | |||
| 7dcb78db97 | |||
| 26984f4452 | |||
| f32d0678df | |||
| 05b99ab6c5 | |||
|
|
88a0098314 | ||
|
|
8b53899648 | ||
|
|
e2b0eaba37 | ||
|
|
e1504dbf72 | ||
|
|
b67b2260fe | ||
|
|
06fd26e641 | ||
|
|
74c74cbbdf | ||
|
|
774ee0d7c7 | ||
|
|
45b3c56ce2 | ||
|
|
b0fa104642 | ||
|
|
d4cd305c28 | ||
|
|
671783fd16 | ||
|
|
674710986f | ||
|
|
77a6ee9747 | ||
|
|
1f2bf02d02 | ||
|
|
c5b8c93a39 | ||
| b9251c3e19 | |||
| 0d1ce9feb3 | |||
| 0673017765 | |||
| 08c389026a | |||
| bbb2ed8f73 | |||
| fe33fe2044 | |||
| a449c792c1 | |||
| 9323111d0d | |||
| c2326414b4 | |||
| 747ad77f4c | |||
| f77a4574ac | |||
| c64c1947e6 | |||
| 1b444dab93 | |||
| 50bf0a2a78 | |||
| 2335e8e23a | |||
| d02a010083 | |||
| 6676347353 | |||
| c9391d840b | |||
| 51323b4a64 | |||
| 842ed76345 | |||
| f3ed658dd0 | |||
| 6c83b8395b | |||
| e58524ce19 | |||
| 9dced21554 | |||
| 3fdf55b53c | |||
| f5b28e5a21 | |||
| 7a380d7df5 | |||
| f7d6348ea9 | |||
| 46871299ca | |||
| c281746fdc | |||
| be7532dd74 | |||
| 108b53a7e2 | |||
| 38793efe6f | |||
| 4205188855 | |||
| d2daed27a8 | |||
| 87b69d5ee8 | |||
| e0d46aa87b | |||
| dbe55be63f | |||
| 28f9e7faae | |||
| 820d3b1c8b | |||
| 278648aae6 | |||
| 9d9491253b | |||
| 499c6375f6 | |||
| 2b150060f4 | |||
| b93cb73e0a | |||
| d2d584d509 | |||
| 8bcb408a5b | |||
| 445b030160 | |||
| 8195808500 | |||
| 393ad136f6 | |||
| 00acfef28c | |||
| efeed2b668 | |||
| efc6379bb3 | |||
| 20a456591d | |||
| 1b301ff2c0 | |||
| 50c58e22ff | |||
| ad2f08698d | |||
| e05ab3beaf | |||
| 48b6d4f149 | |||
| 89edf290ff | |||
| c77096b0f9 | |||
| e333d0e9db | |||
| 324c1b1a17 | |||
|
|
ee3511c09a | ||
|
|
41c262d157 | ||
|
|
837491fa4a | ||
|
|
5a2f61d261 | ||
| 851cdfa924 | |||
| 8dd5e7158f | |||
| 9e52cc2351 | |||
| ca8119f356 | |||
| bef29f5b22 | |||
| e27d63cf32 | |||
| fd46faec21 | |||
| 648531fcc4 | |||
| dda062d959 | |||
| 02c62d0dd8 | |||
| 2249e0fb2e | |||
| f626228c2f | |||
| b9fd5e172c | |||
| 3b05061a31 | |||
| 7ea72e13ac | |||
| faa8e93d55 | |||
| c5ba90202d | |||
| 5fba560bec | |||
| f2059e0dd7 | |||
| e02466771a | |||
| 0b8c435f7c | |||
| fc50aec996 | |||
| bc72243c3e | |||
| c723a1f265 | |||
| e772e6d2b9 | |||
| 48ed84b7bd | |||
| 9cfab29e6c | |||
| c177afe078 | |||
| 69cdc30ae9 | |||
| 0d33be8637 | |||
| 9f709e5a2e | |||
| 12cb8aa15b | |||
| c96488b1eb | |||
| 51bb14862d | |||
| 4cffc9b424 | |||
| 4ea0afe9f6 | |||
| de049d9d33 | |||
| 26a59b4ac2 | |||
| 6a3daa397b | |||
| 7f0c51edc4 | |||
| 46a68d9dd4 | |||
| ff76673ee1 | |||
| b0b1856c01 | |||
| 9e40add0d5 | |||
| 2886c0d629 | |||
| ed2ce83b58 | |||
| 334451238b | |||
| 48ebbebee1 | |||
| bd924f9e23 | |||
|
|
3633b418ea | ||
|
|
46382e2aed | ||
|
|
be7dc9bb91 | ||
|
|
d010b51687 | ||
|
|
07c0a15369 | ||
| 6e091299e4 | |||
| 74882904a0 | |||
| 7639c6909a | |||
| b58066d563 | |||
| 14df8f1e8d | |||
| 188e4568da | |||
| 5006d81d00 | |||
| 97e5404069 | |||
| aa9aca8f6f | |||
| dddc4baa5d | |||
| 3d2c14ef41 | |||
| 628cfea773 | |||
| d2d1fc7e50 | |||
| 27f89f3820 | |||
| ecac0cdc2e | |||
| 5fd338906c | |||
| 88476e0409 | |||
| 7848923445 | |||
| 3b51a35e2c | |||
| 7686f04023 | |||
| 31d917c56e | |||
| bc71887726 | |||
| 89c91b2291 | |||
| 4a6bc2f8aa | |||
| ac5b75c0d6 | |||
| 7e1a2a7ed6 | |||
| e98aa6167e | |||
| 17ddd1c332 | |||
| b0b65e808d | |||
| 0a99178416 | |||
| 91cb420743 | |||
| f4a62eafc2 | |||
| dc5ac98c68 | |||
| b741f331e9 | |||
| 04a528ce55 | |||
| ac7833a069 | |||
| 27eec196a0 | |||
| c6d8d2c746 | |||
| 71ac41086c | |||
| da3a2a0a4b | |||
| 0203644b3e | |||
| dd43d069d5 | |||
| 7dc5eff14b | |||
| 314ac766e4 | |||
| 7456850dec | |||
| 30de7d0784 | |||
| 3a892f6f7a | |||
| b8be4f7314 | |||
| 5514bf89d9 | |||
| 326966b774 | |||
| 8ad378b649 | |||
| 41f977d262 | |||
| 5b04f39bfe | |||
| 71dba46565 | |||
| c16749bdf6 | |||
| 1d107d2de6 | |||
| 5b445cbe89 | |||
| 3482f5adde | |||
| 3dd73d3a63 | |||
| 3b4a06709a | |||
| 5ffb114532 | |||
| 590aef2ece | |||
| e3a2a78b21 | |||
| d0bf3b7ec7 | |||
| 2af3995ae7 | |||
| 02bdba3891 | |||
| 8ba1c9b6b2 | |||
| e188cc35e3 | |||
| 75cbe2717f | |||
| 4c63e80b6c | |||
| 354198c413 | |||
| 56c3f9c8e1 | |||
| b59c3b68ec | |||
| 47eef8aad0 | |||
| d4e0af2c29 | |||
| 9e4b0ecb27 | |||
| 64220d0818 | |||
| 55af7dccec | |||
| 3bddd4385d | |||
| d8927d90c2 | |||
| afad073d0c | |||
| 15b3fcedf1 | |||
| 3f56f9afdc | |||
| c541c45e9b | |||
| 459079d708 | |||
|
|
220581579b | ||
|
|
254e98b37b | ||
|
|
652143d738 | ||
| 5a74edd29f | |||
|
|
7f447077b0 | ||
|
|
b813c32756 | ||
|
|
cdd2e9a1ef | ||
|
|
64bee6c92a | ||
|
|
870ebb06f8 | ||
|
|
2f7cd3d7da | ||
| 18ddcf681f | |||
|
|
ede759b50b | ||
| 9f74bc91a9 | |||
| 8922dbcc57 | |||
| 095a2bdce0 | |||
| b807e4b636 | |||
| 9c17843dec | |||
| c1aafc4223 | |||
| a8d06959b5 | |||
| dbbb25da32 | |||
| 9f180742cd | |||
| e8c1a80264 | |||
| 7e16c77cb6 | |||
| 8f6a4396db | |||
| 34fbbc91a0 | |||
| 9be49ac9c6 | |||
| c96b22fe62 | |||
| 7f50d3c3e2 | |||
| 191cba4946 | |||
| a38f627ca4 | |||
| f31b753182 | |||
| 1377d02520 | |||
| bac1ad0afc | |||
| fa434b180d | |||
| 4d169e6303 | |||
| e1d7822b7c | |||
| cc611828b9 | |||
| 9aead51481 | |||
| 67d00efb10 | |||
| b71fef147a | |||
| 8004dc2b43 | |||
| c9f08ea48e | |||
| 52b88a3ba4 | |||
| e39d753a85 | |||
| 83025065b3 | |||
| 0c65e9de1b | |||
| 1559bf081e | |||
| 79305646dd | |||
| b9ca9f7b45 | |||
| 5aed999671 | |||
| 56b80038a3 | |||
| 3d3c59d58b | |||
| 76bbdbd2f4 | |||
| 22b15c387b | |||
| bcfb838b5e | |||
| 63a16392d7 | |||
| d3ef5ff4c1 | |||
| f45ef7c8b2 | |||
| 8228cc2b1a | |||
| 6b9234dfac | |||
| f150f6d863 | |||
| 7521f15b63 | |||
| 88eb22d454 | |||
| c1c6a4cead | |||
| 845751fca1 | |||
| 2dc0153b10 | |||
| 7143af14f0 | |||
| 51a3d0d19b | |||
| 53b1d0fbf3 | |||
| 20fd6a930b | |||
| 8f0af8d544 | |||
| 16161ebfb8 | |||
| ca224feafb | |||
| 0909842c37 | |||
| 9c785423a9 | |||
|
|
d1f545bb59 | ||
|
|
584e24d751 | ||
|
|
ea25eca9fa | ||
|
|
b8f0eea848 | ||
|
|
b417f37780 | ||
|
|
f52b684722 | ||
|
|
5c15f89f59 | ||
|
|
f2e2997988 | ||
|
|
1589c7fe29 | ||
|
|
5e23180c7c | ||
|
|
f8eafed093 | ||
|
|
debd0dd4af | ||
|
|
04d425b88d | ||
|
|
ca21dc3dd9 | ||
| f4d4b6a75a | |||
|
|
4ff3b24df8 | ||
|
|
1da7236d5d | ||
|
|
df14b8b227 | ||
|
|
15508013a3 | ||
|
|
cb47336230 | ||
| 76cabe058b | |||
| 33175ffdcb | |||
| 54cf5eff2b | |||
| 94bba45f45 | |||
| 23ef7e3ded | |||
| a84d93882d | |||
| 0ec6888809 | |||
| 977d2325fa | |||
| 20975a2872 | |||
| 61cf886174 | |||
| bf638ca995 | |||
| a599648d87 | |||
| e5fc1a017e | |||
| 44394daebf | |||
| ac76aab168 | |||
| 4f56acb320 | |||
| 70fe586fc6 | |||
| 58bd4ee766 | |||
| 3d789d68e7 | |||
| 9327a739f8 | |||
| de731095cc | |||
| 696ea613de | |||
| 9b17b6b231 | |||
| b1d5356220 | |||
| 37dad0d1d7 | |||
| c5e41f2c05 | |||
| d45a915ba9 | |||
| b7d1912fc6 | |||
| d51efe8b3c | |||
| 48a97beb74 | |||
| e94406932b | |||
| 4fe1fb0da2 | |||
| dcc8eba931 | |||
| 4c90e553af | |||
| b8f75f9529 | |||
| d10e062373 | |||
| 3c1d2ef14b | |||
| b58c8ebce8 | |||
| d602d2d34b | |||
| de938e6d1e | |||
| 949dec9bdc | |||
| 89c4bfe13f | |||
| 7a0b72584c | |||
| ebfab19df4 | |||
| 77630f4e83 | |||
| 16619f9631 | |||
| 9829e02a38 | |||
| 632ca0d3e4 | |||
| 4d436620a5 | |||
| 78da2b7207 | |||
| d4a8b272f2 | |||
| bb01f6526e | |||
| 866d01c4e2 | |||
| 2159eaec08 | |||
| bf3e5e619b | |||
|
|
458cf70081 | ||
|
|
3d81b7a97e | ||
|
|
03f52cfb5b | ||
|
|
4b0a6df5dd | ||
|
|
744bc9e4d0 | ||
|
|
76ba4fd74a | ||
|
|
132e3d2930 | ||
|
|
7554dbac9c | ||
|
|
defd97e7f0 | ||
|
|
6c9a608ce6 | ||
|
|
dc1e44f2be | ||
| 923aaa6865 | |||
| 53fe2cc3d5 | |||
| 970f99db54 | |||
| 96f431fb41 | |||
| 604f0e33a0 | |||
| 6d75843dcd | |||
| 0cb5d097d6 | |||
| 0549be6d42 | |||
| 5cd16a7416 | |||
| 5573be1502 | |||
| 99ef2db822 | |||
| 0502cbbd76 | |||
| 6d29f053f4 | |||
|
|
75c667d1c2 | ||
|
|
740941790b | ||
| 5511bc56f2 | |||
| d90a9421e9 | |||
| ba1071ba6c | |||
| 29b021bee5 | |||
| 07fcd05031 |
@@ -22,4 +22,5 @@
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
README.md
|
||||
/storageImages
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,4 +4,9 @@ obj/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea/.idea.MilkyShots/Docker/docker-compose.generated.override.yml
|
||||
storageImages/
|
||||
storageImages/
|
||||
dotnet-tools.json
|
||||
.opencode/
|
||||
*.css.map
|
||||
# SixLabors license file
|
||||
/Lactose/sixlabors.lic
|
||||
384
.idea/.idea.BlazorTest/.idea/workspace.xml
generated
384
.idea/.idea.BlazorTest/.idea/workspace.xml
generated
@@ -1,384 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AnalysisUIOptions">
|
||||
<option name="SCOPE_TYPE" value="3" />
|
||||
</component>
|
||||
<component name="AutoGeneratedRunConfigurationManager">
|
||||
<projectFile profileName="IIS Express">BlazorTest/BlazorTest.csproj</projectFile>
|
||||
<projectFile profileName="http">BlazorTest/BlazorTest.csproj</projectFile>
|
||||
<projectFile profileName="https">BlazorTest/BlazorTest.csproj</projectFile>
|
||||
<projectFile profileName="IIS Express">WepApiTest/WepApiTest.csproj</projectFile>
|
||||
<projectFile profileName="http">WepApiTest/WepApiTest.csproj</projectFile>
|
||||
<projectFile profileName="https">WepApiTest/WepApiTest.csproj</projectFile>
|
||||
</component>
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8146fea4-834d-47b3-9aaa-32befb744c64" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="DatabaseLocalColorSettings">
|
||||
<colors>
|
||||
<entry key="eab007eb-4f50-4961-9bce-5c4940a7cdff" value="Blue" />
|
||||
</colors>
|
||||
</component>
|
||||
<component name="DpaMonitoringSettings">
|
||||
<option name="autoShow" value="false" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="HTTP Public Environment File" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="jar://$APPLICATION_HOME_DIR$/plugins/restClient/lib/restClient.jar!/com/intellij/ws/rest/client/requests/collection/post-requests.http" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="jar://$APPLICATION_HOME_DIR$/plugins/restClient/lib/restClient.jar!/com/intellij/ws/rest/client/requests/collection/requests-with-authorization.http" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/1013dc174d8e4a313727d545767084e3df6affa8d4723b41a5f7881e96b1d/ControllerContext.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/36f346b6c0454bc8a6afa7aed38119fe5bbffcc983298d9bfa4dbd5f49461f/Monitor.CoreCLR.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/3bd4df5aff92cabbc4d630be64227073db1b8539b3a1e47786b4b189d7cdb7/DbContext.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/449b441523c469ed34ff5a5e14f0bafcd8f097aa463655303dc19048fa44ac3/EntityFrameworkServiceCollectionExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7b3b6985aad11c5d633d75b545f5f5f4fc7e50918ad1cb9f6b615682f5bfb76d/SingleQueryingEnumerable.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/96a4cc66aa48dda8a9d8be8febec4a769fc143f7a86dd53ecdebfb9c3177d/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/9e6ecf3790a24c43487ae724bf9ea8185bdedaf6c28da376d6ea862e66453/WebApplicationBuilder.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a42ba4db448ed38dcb9fb39194c637a317a8281856b88e139e8a905ee6d12b/Guid.Windows.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/WebApiTest/GlobalUsings.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/WebApiTest/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="HttpClientSelectedEnvironments">
|
||||
<file url="jar://$APPLICATION_HOME_DIR$/plugins/restClient/lib/restClient.jar!/com/intellij/ws/rest/client/requests/collection/post-requests.http" environment="test" />
|
||||
<file url="file://$USER_HOME$/AppData/Local/Temp/whats-new-dir/whats-new.http" environment="test" />
|
||||
<file url="jar://$APPLICATION_HOME_DIR$/plugins/restClient/lib/restClient.jar!/com/intellij/ws/rest/client/requests/collection/requests-with-authorization.http" environment="test" />
|
||||
</component>
|
||||
<component name="KubernetesApiPersistence">{}</component>
|
||||
<component name="KubernetesApiProvider">{
|
||||
"isMigrated": true
|
||||
}</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="CurrentFile" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 4
|
||||
}</component>
|
||||
<component name="ProjectId" id="2mIMBmlQ4hFJsyahkv7dCj0zoOT" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||
<ConfirmationsSetting value="2" id="Add" />
|
||||
</component>
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.BlazorTest: http.executor": "Debug",
|
||||
".NET Launch Settings Profile.BlazorTest: https.executor": "Debug",
|
||||
".NET Launch Settings Profile.WepApiTest: http.executor": "Debug",
|
||||
"Docker.MariaDbDev.executor": "Run",
|
||||
"Docker.WebApiDocker.executor": "Run",
|
||||
"Docker.WebApiTest/Dockerfile.executor": "Debug",
|
||||
"HTTP Request.WepApiTest | #1 (1).executor": "Run",
|
||||
"HTTP Request.WepApiTest | #1.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.lookFeel",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"mariadb"
|
||||
]
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.WepApiTest: http">
|
||||
<configuration name="WepApiTest | #1 (1)" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" temporary="true" nameIsGenerated="true" path="$PROJECT_DIR$/WebApiTest/WepApiTest.http" requestIdentifier="#1" runType="Run single request">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="BlazorTest: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BlazorTest/BlazorTest.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="IIS Express" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="BlazorTest: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BlazorTest/BlazorTest.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="BlazorTest: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/BlazorTest/BlazorTest.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="WepApiTest: IIS Express" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Lactose/Lactose.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="IIS Express" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="WepApiTest: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Lactose/Lactose.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="WepApiTest: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/Lactose/Lactose.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net8.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="BlazorTest/Dockerfile" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="blazortest" />
|
||||
<option name="containerName" value="blazortest" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="BlazorTest/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="MariaDbDev" type="docker-deploy" factoryName="docker-image" temporary="true" server-name="Docker">
|
||||
<deployment type="docker-image">
|
||||
<settings>
|
||||
<option name="imageTag" value="mariadb:latest" />
|
||||
<option name="containerName" value="MariaDbDev" />
|
||||
<option name="envVars">
|
||||
<list>
|
||||
<DockerEnvVarImpl>
|
||||
<option name="name" value="MYSQL_ROOT_PASSWORD" />
|
||||
<option name="value" value="testOnlyDb" />
|
||||
</DockerEnvVarImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="3306" />
|
||||
<option name="hostPort" value="3306" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
</settings>
|
||||
</deployment>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="WebApiDocker" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="wepapitest" />
|
||||
<option name="containerName" value="wepapitest" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="5162" />
|
||||
<option name="hostPort" value="5162" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="WebApiTest/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="docker-compose.yml" temporary="true">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings />
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="docker-image" temporary="true">
|
||||
<deployment type="docker-image">
|
||||
<settings />
|
||||
</deployment>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
|
||||
<deployment type="dockerfile">
|
||||
<settings />
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="docker-compose.yml: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="envFilePath" value="" />
|
||||
<option name="sourceFilePath" value="docker-compose.yml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue=".NET Launch Settings Profile.BlazorTest: http" />
|
||||
<item itemvalue=".NET Launch Settings Profile.BlazorTest: https" />
|
||||
<item itemvalue=".NET Launch Settings Profile.BlazorTest: IIS Express" />
|
||||
<item itemvalue=".NET Launch Settings Profile.WepApiTest: http" />
|
||||
<item itemvalue=".NET Launch Settings Profile.WepApiTest: https" />
|
||||
<item itemvalue=".NET Launch Settings Profile.WepApiTest: IIS Express" />
|
||||
<item itemvalue="Docker.BlazorTest/Dockerfile" />
|
||||
<item itemvalue="Docker.docker-compose.yml: Compose Deployment" />
|
||||
<item itemvalue="Docker.WebApiDocker" />
|
||||
<item itemvalue="Docker.MariaDbDev" />
|
||||
<item itemvalue="HTTP Request.WepApiTest | #1 (1)" />
|
||||
</list>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="HTTP Request.WepApiTest | #1 (1)" />
|
||||
<item itemvalue="Docker.MariaDbDev" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="8146fea4-834d-47b3-9aaa-32befb744c64" name="Changes" comment="" />
|
||||
<created>1726759505353</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1726759505353</updated>
|
||||
<workItem from="1726759506537" duration="8762000" />
|
||||
<workItem from="1726775138143" duration="20326000" />
|
||||
<workItem from="1726877247262" duration="2104000" />
|
||||
<workItem from="1726879467881" duration="2586000" />
|
||||
<workItem from="1726882140090" duration="5969000" />
|
||||
<workItem from="1726888243937" duration="249000" />
|
||||
<workItem from="1726888519041" duration="1373000" />
|
||||
<workItem from="1727188917179" duration="1894000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="BaseFiles">
|
||||
<option name="closed" value="true" />
|
||||
<created>1726786753746</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1726786753746</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="BaseFiles">
|
||||
<option name="closed" value="true" />
|
||||
<created>1726786763975</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1726786763975</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="3" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
<MESSAGE value="BaseFiles" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="BaseFiles" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
<select />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/.idea.MilkyShots/.idea/discord.xml
generated
2
.idea/.idea.MilkyShots/.idea/discord.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
<option name="applicationTheme" value="default" />
|
||||
<option name="iconsTheme" value="default" />
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
</option>
|
||||
<option name="removeOrphansOnComposeDown" value="false" />
|
||||
<option name="commandLineOptions" value="--build" />
|
||||
<option name="secondarySourceFiles">
|
||||
<list>
|
||||
<option value="docker-compose.pm.yml" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="docker-compose.yml" />
|
||||
<option name="upRemoveOrphans" value="true" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="false" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,35 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Lactose (NoCache)" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="lactose" />
|
||||
<option name="buildCliOptions" value="--no-cache " />
|
||||
<option name="containerName" value="lactose" />
|
||||
<option name="envVars">
|
||||
<list>
|
||||
<DockerEnvVarImpl>
|
||||
<option name="name" value="ASPNETCORE_ENVIRONMENT" />
|
||||
<option name="value" value="Docker" />
|
||||
</DockerEnvVarImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="5162" />
|
||||
<option name="hostPort" value="5162" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="Lactose/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="false" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,36 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Lactose" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="lactose" />
|
||||
<option name="containerName" value="lactose" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="envVars">
|
||||
<list>
|
||||
<DockerEnvVarImpl>
|
||||
<option name="name" value="ASPNETCORE_ENVIRONMENT" />
|
||||
<option name="value" value="Docker" />
|
||||
</DockerEnvVarImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="5162" />
|
||||
<option name="hostPort" value="5162" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="showCommandPreview" value="true" />
|
||||
<option name="sourceFilePath" value="Lactose/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="false" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,23 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MilkStream/Dockerfile" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="milkstream" />
|
||||
<option name="containerName" value="milkstream" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="MilkStream/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
153
AGENTS.md
153
AGENTS.md
@@ -1,23 +1,59 @@
|
||||
# MilkyShots – Agent Guide
|
||||
|
||||
## Projects (4 in solution)
|
||||
**Gitea repo owner:** MilkyShots
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **.NET 10 SDK** (pinned in `global.json`; `allowPrerelease: true`)
|
||||
- **PostgreSQL with pgvector extension**
|
||||
- **SCSS** compiled by `AspNetCore.SassCompiler` MSBuild task (no Node.js needed; compiles during `dotnet build`)
|
||||
- Run `dotnet tool restore` before first migration — installs `dotnet-ef` (tool manifest at `dotnet-tools.json` in repo root)
|
||||
|
||||
## SCSS
|
||||
|
||||
SCSS source is in `MilkStream.Client/Styles/`. Compiled via `AspNetCore.SassCompiler` MSBuild target:
|
||||
|
||||
```bash
|
||||
dotnet build # compiles SCSS automatically
|
||||
```
|
||||
|
||||
- Config: `MilkStream.Client/Styles/sasscompiler.json` (expanded in Debug, compressed in Release)
|
||||
- Source maps disabled
|
||||
- Bootstrap 5.3.7 SCSS vendored in `Styles/bootstrap/`
|
||||
- Theme variables adjustable in `Styles/_variables.scss` (`$accent` for primary color)
|
||||
- Aggressive gradient mixins in `Styles/_gradients.scss`
|
||||
- Works in Docker — MSBuild task runs during `dotnet build`/`dotnet publish`, no hosted service
|
||||
|
||||
## Projects (5 in solution)
|
||||
|
||||
| Project | Role | Entrypoint |
|
||||
|---|---|---|
|
||||
| **Butter** | Shared class library (DTOs, enums, MIME types) | — |
|
||||
| **Lactose.Analyzers** | Roslyn analyzer — enforces `DateTime.UtcNow` (not `DateTime.Now`) as error MS001 | — |
|
||||
| **Lactose** | ASP.NET Core Web API — controllers, EF Core, repos, background jobs | `Lactose/Program.cs` |
|
||||
| **MilkStream** | Blazor WASM host — serves WASM files + dynamic `/appsettings.json` | `MilkStream/Program.cs` |
|
||||
| **MilkStream.Client** | Blazor WASM client — Razor components, SCSS, frontend services | `MilkStream.Client/Program.cs` |
|
||||
| **MilkStream.Client** | Blazor WASM client — Razor components, SCSS, frontend services, shared UI components (ModalFrame, EmptyState) | `MilkStream.Client/Program.cs` |
|
||||
|
||||
## Build & run
|
||||
|
||||
```bash
|
||||
dotnet build MilkyShots.sln # .NET 8, C# 12
|
||||
dotnet build MilkyShots.sln # .NET 10, C# 12
|
||||
dotnet run --project Lactose # API on :5162 (host) / :8080 (container)
|
||||
dotnet run --project MilkStream # WASM host on :8080
|
||||
dotnet run --project MilkStream # WASM host on :5269 (host) / :8080 (container)
|
||||
```
|
||||
|
||||
**Prerequisite:** PostgreSQL with pgvector extension. Default DB credentials in `appsettings.json`. Docker compose spins up all services.
|
||||
**Database:** Default credentials in `appsettings.json`. Docker compose spins up all services.
|
||||
|
||||
**Port gotcha:** `appsettings.json` defaults `DatabaseAddress:Port` to **3306** (legacy MySQL port). PostgreSQL runs on 5432. The raw connection string must be overridden, or use `docker-compose.yml` which maps host 3306 → container 5432 automatically.
|
||||
|
||||
## Infrastructure gotchas
|
||||
|
||||
- **MilkStream `/appsettings.json`:** Dynamically generated from server config — maps **before** `UseStaticFiles`, so it shadows the static file in `MilkStream.Client/wwwroot/`. This endpoint provides `LactoseBaseUrl` to the WASM client.
|
||||
- **Auth requires claims transformation:** `RefreshTokenTransformation` (registered as `IClaimsTransformation`) must succeed on every authenticated request, or all endpoints return 403.
|
||||
- **`dotnet-ef` CLI vs Microsoft.EntityFrameworkCore.Tools:** The global `dotnet-ef` (from `dotnet-tools.json`, v10.0.9) is the correct CLI to use. The Tools package reference in `Lactose.csproj` (v10.0.9) provides the `dotnet ef` target for `dotnet build`-time validation.
|
||||
- **WASM client DI:** `LoginService` is **Singleton**; all other services are `Scoped`. Two `HttpClient` registrations: one unauthenticated (default) for login, and one named `"MilkstreamClient"` which has `JwtTokenRefresher` as a `DelegatingHandler`. The handler transparently refreshes expired tokens pre-flight and retries once on 401 — authenticated services must **never** handle token refreshes manually. Do not add `SendWithRefreshAsync` or similar wrappers; rely solely on the named client.
|
||||
- **HTTPS redirection is commented out** in Lactose — API does not enforce HTTPS.
|
||||
- **DateTime analyzer:** `Lactose.Analyzers` treats `DateTime.Now` as error MS001, `DateTime.Today` as error MS002, `DateTime.Date` as warning MS003, and non-UTC `SpecifyKind` as error MS004. Always use `DateTime.UtcNow`. All timestamp columns use `timestamp with time zone`.
|
||||
|
||||
## XML docs enforced
|
||||
|
||||
@@ -26,11 +62,62 @@ dotnet run --project MilkStream # WASM host on :8080
|
||||
## Conventions
|
||||
|
||||
- **`HttpPut` = create, `HttpPost("{id}")` = update** — not REST-idiomatic; do not "fix" without team buy-in
|
||||
- **Soft delete only** — set `DeletedAt`, never hard-delete
|
||||
- **Soft delete only** — set `DeletedAt`, never hard-delete (models: Asset, User)
|
||||
- **Repository `Save()` must be called explicitly** after insert/update/delete
|
||||
- **Enum prefix `E`** (e.g. `EAccessLevel`, `EAssetType`, `EJobStatus`)
|
||||
- **JWT access token**: 10 min expiry, refresh token: 60 min
|
||||
- **JWT access token**: 10 min, refresh token: 60 min (constants in `LactoseAuthService.cs`)
|
||||
- **Pagination is zero-based** everywhere — `Page = 0` is the first page. All repositories use `Skip(page * pageSize)`. All controllers validate `Page < 0` (reject negative) and `PageSize < 1 || PageSize > 250` (clamp 1–250).
|
||||
- **Conventional Commits** enforced via `cliff.toml`; changelog generated with `git-cliff`
|
||||
- **Granular commits** — commit each logical change separately (e.g., DTO change, controller logic, UI component) with a descriptive commit message
|
||||
- **Reuse API endpoints** — prefer adding optional query parameters to existing endpoints over creating new routes. New endpoints are a last resort when the existing ones fundamentally cannot serve the need.
|
||||
|
||||
## Visibility Rules (R1–R7)
|
||||
|
||||
Defined in issue [#129](https://git.r3d.codes/MilkyShots/MilkyShots/issues/129). Each entity (Asset, Album, Person) carries **independent** `EVisibility` — no inheritance from container to child.
|
||||
|
||||
### R1 — Independent Visibility
|
||||
No entity's visibility is overridden by its container. An asset in a Public album does NOT become Public; the asset is only visible if the viewer has permission for the asset's own visibility.
|
||||
|
||||
### R2 — Asset Visibility (Per-Asset Gate)
|
||||
|
||||
| Actor | Public | Protected | Private |
|
||||
|---|---|---|---|
|
||||
| Anonymous | ✅ | ❌ | ❌ |
|
||||
| User | ✅ | ✅ | ❌ |
|
||||
| Maintainer (not owning / not linked) | ✅ | ✅ | ❌ |
|
||||
| Maintainer (uploaded by self) | ✅ | ✅ | ✅ |
|
||||
| Maintainer (album → person they maintain) | ✅ | ✅ | ✅ |
|
||||
| Curator (non-deleted) | ✅ | ✅ | ✅ |
|
||||
| Curator (deleted, own upload) | ✅ | ✅ | ✅ |
|
||||
| Admin (all, including deleted) | ✅ | ✅ | ✅ |
|
||||
|
||||
### R3 — Album Access Gating
|
||||
Album's own visibility determines who can access it. Not visible → 404.
|
||||
|
||||
| Actor | Public | Protected | Private |
|
||||
|---|---|---|---|
|
||||
| Anonymous / User | ✅ | ✅ | ❌ |
|
||||
| Maintainer (unassigned) | ✅ | ✅ | ❌ |
|
||||
| Maintainer (maintains PersonOwner) | ✅ | ✅ | ✅ |
|
||||
| Curator / Admin | ✅ | ✅ | ✅ |
|
||||
|
||||
### R4 — Person Access Gating
|
||||
Same as R3. Not visible → 404.
|
||||
|
||||
### R5 — Media Serving
|
||||
Uses per-asset rules (R2). Album provenance not checked — too complex, marginal benefit.
|
||||
|
||||
### R6 — Edit Mode (Frontend)
|
||||
API returns Private assets to privileged users (Admin/Curator/Maintainer-on-owned-content). Frontend hides them in browse mode, shows them in edit/curator mode. No backend flag.
|
||||
|
||||
### R7 — Deleted Assets
|
||||
- **Admin**: all deleted assets visible
|
||||
- **Curator**: own deleted uploads visible
|
||||
- **Everyone else**: deleted assets hidden (`DeletedAt IS NULL`)
|
||||
|
||||
Reusable UI components live in `MilkStream.Client/Components/Shared/`.
|
||||
When a markup pattern appears in 2+ places, extract it into a shared component.
|
||||
See issue #52 for the tracking list of candidate components.
|
||||
|
||||
## CORS gotchas
|
||||
|
||||
@@ -47,6 +134,56 @@ dotnet ef migrations add <Name> --project Lactose
|
||||
dotnet ef database update --project Lactose
|
||||
```
|
||||
|
||||
**Database indexes for performance** should be defined using the **fluent API** in `LactoseDbContext.OnModelCreating` via `HasIndex().HasFilter()`. Do not use the `[Index]` data annotation — the `Filter` parameter is not available in the current package configuration (EF Core Tools v10.0.9 vs Design v10.0.9 mismatch). Example:
|
||||
|
||||
```csharp
|
||||
modelBuilder.Entity<Asset>().HasIndex(e => new { e.Visibility, e.UploadedBy })
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
```
|
||||
|
||||
- **Trigram GIN indexes** on `People.Name` and `Albums.Title` use `HasMethod("gin")` with `HasOperators("gin_trgm_ops")` for case-insensitive `ILike` search.
|
||||
- **pgvector and pg_trgm** extensions are enabled in `OnModelCreating` via `HasPostgresExtension`.
|
||||
|
||||
## Tests
|
||||
|
||||
No test project found in solution.
|
||||
No unit/integration test project exists. API endpoint testing is done via **REST Client `.http` files**:
|
||||
|
||||
- **`Lactose/WepApiTest.http`** — 100+ numbered tests covering all API endpoints across 12 sections (Auth → User CRUD → Person → Album → Tag → Asset → Stats → Settings → Data Visibility → Visibility Gates → Cleanup). Tests verify authorization at every level (anonymous, user, curator, admin) and include comprehensive visibility gate tests (R2–R4). Self-contained — all test data is created at runtime and cleaned up.
|
||||
- **`Lactose/http-client.env.json`** — provides pre-filled variable values (`userName`, `curatorName`, etc.) for the REST Client extension.
|
||||
- Open in VS Code/Rider with the REST Client extension and run individual tests or the full suite.
|
||||
- Server must be running locally (default `http://localhost:5162`).
|
||||
- Paginated list tests (people, albums, users) use `?page=0&pageSize=5` — change these carefully.
|
||||
|
||||
## PR workflow
|
||||
|
||||
- Before starting work, ask the user whether they want the fix in a PR or directly on `develop` (for quick/trivial fixes).
|
||||
- Larger work always goes in a PR.
|
||||
- Always mark the PR as **draft** until the user confirms they are satisfied and have tested it works.
|
||||
- When making the PR ready (removing draft status), mark it ready for review and request:
|
||||
- **REDCODE** when the PR contains frontend changes
|
||||
- **Fastwind** when the PR contains backend changes
|
||||
- **Both** when both areas are touched
|
||||
|
||||
## Data sizes
|
||||
|
||||
Approximate record counts in the database:
|
||||
|
||||
| Entity | Count |
|
||||
|---|---|
|
||||
| Assets | 1,000,000+ |
|
||||
| Albums | 10,000+ |
|
||||
| People | 1,000+ |
|
||||
| Users | 100+ |
|
||||
| Maintainers | 10+ |
|
||||
| Curators | single digit |
|
||||
| Admins | single digit |
|
||||
|
||||
## Gitea labels
|
||||
|
||||
Labels live at two levels:
|
||||
- **Org level** (`MilkyShots` org): generic issue types (`bug`, `enhancement`, `help wanted`, `question`, `duplicate`, `invalid`, `wontfix`)
|
||||
- **Repo level** (`MilkyShots/MilkyShots`): project-specific labels (`area:frontend`, `area:backend`, `page:*`, `type:refactor`, `priority:*`, etc.)
|
||||
- When labelling issues, check both levels with the `label_read` tool (`list_org_labels` / `list_repo_labels`).
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
37
Butter/Dtos/Album/AlbumAssetPreviewDto.cs
Normal file
37
Butter/Dtos/Album/AlbumAssetPreviewDto.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal asset info needed for displaying album assets.
|
||||
/// </summary>
|
||||
public class AlbumAssetPreviewDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the asset ID.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution width.
|
||||
/// </summary>
|
||||
public int ResolutionWidth { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution height.
|
||||
/// </summary>
|
||||
public int ResolutionHeight { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset has a thumbnail.
|
||||
/// </summary>
|
||||
public bool HasThumbnail { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset has a preview.
|
||||
/// </summary>
|
||||
public bool HasPreview { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the original filename of the asset.
|
||||
/// </summary>
|
||||
public string? FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Only populated for curators and admins.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,17 +9,21 @@ public class AlbumCreateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the album.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the owner ID of the album.
|
||||
/// </summary>
|
||||
public Guid? Owner { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the person ID associated with the album.
|
||||
/// </summary>
|
||||
public Guid? Person { get; set; }
|
||||
public Guid? Person { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the cover asset ID for the album.
|
||||
/// </summary>
|
||||
public Guid? CoverAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of asset IDs in the album.
|
||||
/// </summary>
|
||||
public List<Guid>? Assets { get; set; }
|
||||
public List<Guid>? Assets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Defaults to <see cref="EVisibility.Protected"/>.
|
||||
/// </summary>
|
||||
public EVisibility Visibility { get; set; } = EVisibility.Protected;
|
||||
}
|
||||
@@ -8,4 +8,8 @@ public class AlbumFullDto : AlbumPreviewDto {
|
||||
/// Gets or sets the list of asset IDs in the album.
|
||||
/// </summary>
|
||||
public List<Guid> Images { get; set; } = [];
|
||||
/// <summary>
|
||||
/// Gets or sets the asset preview information for rendering.
|
||||
/// </summary>
|
||||
public List<AlbumAssetPreviewDto> AssetPreviews { get; set; } = [];
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,17 +9,29 @@ public class AlbumPreviewDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the album.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the album.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the owner ID of the album.
|
||||
/// </summary>
|
||||
public Guid? Owner { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the person ID associated with the album.
|
||||
/// </summary>
|
||||
public Guid? Person { get; set; }
|
||||
public Guid? Person { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the person name associated with the album.
|
||||
/// </summary>
|
||||
public string? PersonName { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the cover asset for this album.
|
||||
/// </summary>
|
||||
public Guid? CoverAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets in the album.
|
||||
/// </summary>
|
||||
public int AssetCount { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Only populated for curators and admins.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
17
Butter/Dtos/Album/AlbumSearchParametersDto.cs
Normal file
17
Butter/Dtos/Album/AlbumSearchParametersDto.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
/// Search and filter parameters for album queries.
|
||||
/// </summary>
|
||||
public class AlbumSearchParametersDto : PagedSearchParametersDto {
|
||||
/// <summary>
|
||||
/// Gets or sets whether to return only albums with no person assigned.
|
||||
/// </summary>
|
||||
public bool Unassigned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user ID to filter albums by.
|
||||
/// Only returns albums that contain at least one asset uploaded by the specified user.
|
||||
/// </summary>
|
||||
public Guid? AssetUploadedBy { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,17 +9,25 @@ public class AlbumUpdateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the album.
|
||||
/// </summary>
|
||||
public string? Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the owner ID of the album.
|
||||
/// </summary>
|
||||
public Guid? Owner { get; set; }
|
||||
public string? Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the person ID associated with the album.
|
||||
/// </summary>
|
||||
public Guid? Person { get; set; }
|
||||
public Guid? Person { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether to remove the person assignment from the album.
|
||||
/// </summary>
|
||||
public bool RemovePerson { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the cover asset ID for the album.
|
||||
/// </summary>
|
||||
public Guid? CoverAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of asset IDs in the album.
|
||||
/// </summary>
|
||||
public List<Guid>? Assets { get; set; }
|
||||
public List<Guid>? Assets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,41 +9,42 @@ public class AssetCreateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the creation date of the asset.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the owner ID of the asset.
|
||||
/// Gets or sets the ID of the user who uploaded the asset.
|
||||
/// Null for scanner-created assets (system uploader will be used).
|
||||
/// </summary>
|
||||
public Guid Owner { get; set; }
|
||||
public Guid? UploadedBy { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the asset.
|
||||
/// </summary>
|
||||
public EAssetType AssetType { get; set; }
|
||||
public EAssetType AssetType { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset is publicly shared.
|
||||
/// Gets or sets the visibility level. Defaults to <see cref="EVisibility.Protected"/>.
|
||||
/// </summary>
|
||||
public bool IsPublic { get; set; }
|
||||
public EVisibility Visibility { get; set; } = EVisibility.Private;
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the asset.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution width in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionWidth { get; set; }
|
||||
public int ResolutionWidth { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution height in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionHeight { get; set; }
|
||||
public int ResolutionHeight { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the file size in bytes.
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the duration in seconds (for video/audio).
|
||||
/// </summary>
|
||||
public int? Duration { get; set; }
|
||||
public int? Duration { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the frame rate (for video).
|
||||
/// </summary>
|
||||
public int? FrameRate { get; set; }
|
||||
public int? FrameRate { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,56 +7,40 @@ namespace Butter.Dtos.Asset;
|
||||
/// Represents the full information needed to display an asset on a detail page.
|
||||
/// </summary>
|
||||
public class AssetDto : AssetPreviewDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the original filename of the asset. May be null if the user lacks permission.
|
||||
/// </summary>
|
||||
public string? FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the asset (e.g., Image, Video).
|
||||
/// </summary>
|
||||
public EAssetType AssetType { get; set; }
|
||||
public EAssetType AssetType { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset is publicly shared.
|
||||
/// Gets or sets the user who uploaded the asset.
|
||||
/// </summary>
|
||||
public bool IsPublic { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the owner of the asset.
|
||||
/// </summary>
|
||||
public required UserInfoDto Owner { get; set; }
|
||||
public required UserInfoDto UploadedBy { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the creation date of the asset.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the last updated date of the asset.
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the deletion date of the asset.
|
||||
/// </summary>
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the asset.
|
||||
/// </summary>
|
||||
public new string MimeType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution width of the asset in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionWidth { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution height of the asset in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionHeight { get; set; }
|
||||
public new string MimeType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the file size of the asset in bytes.
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the duration of the asset in seconds (for video/audio).
|
||||
/// </summary>
|
||||
public float Duration { get; set; }
|
||||
public float Duration { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the frame rate of the asset (for video).
|
||||
/// </summary>
|
||||
public float FrameRate { get; set; }
|
||||
public float FrameRate { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Asset;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,9 +9,49 @@ public class AssetPreviewDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the asset.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the asset.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution width of the asset in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionWidth { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution height of the asset in pixels.
|
||||
/// </summary>
|
||||
public int ResolutionHeight { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset has a generated thumbnail.
|
||||
/// </summary>
|
||||
public bool HasThumbnail { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset has a generated preview.
|
||||
/// </summary>
|
||||
public bool HasPreview { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of album titles associated with the asset.
|
||||
/// </summary>
|
||||
public List<string>? AlbumNames { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of album IDs associated with the asset, parallel to <see cref="AlbumNames"/>.
|
||||
/// </summary>
|
||||
public List<Guid>? AlbumIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of cosplayer names associated with the asset, derived from album person owners.
|
||||
/// </summary>
|
||||
public List<string>? CosplayerNames { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of cosplayer IDs associated with the asset, parallel to <see cref="CosplayerNames"/>.
|
||||
/// </summary>
|
||||
public List<Guid>? CosplayerIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the original filename of the asset.
|
||||
/// </summary>
|
||||
public string? FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Only populated for curators and admins.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Asset;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,4 +14,16 @@ public class AssetSearchOptionsDto: PagedSearchParametersDto {
|
||||
/// Gets or sets the end date for filtering assets by date range.
|
||||
/// </summary>
|
||||
public string? EndDate { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the optional asset type filter.
|
||||
/// </summary>
|
||||
public EAssetType? Type { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the seed for deterministic random ordering.
|
||||
/// </summary>
|
||||
public Guid? Seed { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether to order results randomly instead of by date.
|
||||
/// </summary>
|
||||
public bool Random { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Asset;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,15 +7,15 @@ namespace Butter.Dtos.Asset;
|
||||
/// </summary>
|
||||
public class AssetUpdateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the new owner ID for the asset.
|
||||
/// Gets or sets the ID of the user who uploaded the asset.
|
||||
/// </summary>
|
||||
public Guid? Owner { get; set; }
|
||||
public Guid? UploadedBy { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the deletion date to set on the asset.
|
||||
/// </summary>
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the asset is publicly shared.
|
||||
/// Gets or sets the visibility level.
|
||||
/// </summary>
|
||||
public bool? IsPublic { get; set; }
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
|
||||
15
Butter/Dtos/Jobs/ChildrenResponse.cs
Normal file
15
Butter/Dtos/Jobs/ChildrenResponse.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Butter.Dtos.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response containing child jobs and a timestamp for delta-based polling.
|
||||
/// </summary>
|
||||
public class ChildrenResponse {
|
||||
/// <summary>
|
||||
/// Gets the list of child job statuses.
|
||||
/// </summary>
|
||||
public List<JobStatusDto> Children { get; init; } = [];
|
||||
/// <summary>
|
||||
/// Gets the server timestamp to use as <c>since</c> for the next delta poll.
|
||||
/// </summary>
|
||||
public DateTime Since { get; init; }
|
||||
}
|
||||
@@ -63,7 +63,11 @@ public enum EJobType {
|
||||
/// <summary>Performs an integrity check on the system.</summary>
|
||||
IntegrityCheck,
|
||||
/// <summary>Computes perceptual hashes for assets.</summary>
|
||||
PHashGeneration
|
||||
PHashGeneration,
|
||||
/// <summary>Creates missing Person entities extracted from asset paths via folder regex patterns.</summary>
|
||||
CreatePersons,
|
||||
/// <summary>Creates missing Person and Album entities extracted from asset paths via folder regex patterns.</summary>
|
||||
CreateAlbums
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,6 +4,12 @@ namespace Butter.Dtos;
|
||||
/// Represents pagination parameters for a query.
|
||||
/// </summary>
|
||||
public class PagedParametersDto {
|
||||
|
||||
/// <summary>
|
||||
/// Contains the global constant for max pages.
|
||||
/// </summary>
|
||||
public const int MaxPageSize = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the page number (zero-based).
|
||||
/// </summary>
|
||||
@@ -12,6 +18,16 @@ public class PagedParametersDto {
|
||||
/// Gets or sets the number of items per page.
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 150;
|
||||
/// <summary>
|
||||
/// Gets or sets the field to sort by (e.g. "name", "created").
|
||||
/// When <c>null</c>, the default sort order is applied.
|
||||
/// </summary>
|
||||
public string? SortBy { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the sort direction.
|
||||
/// <c>false</c> = descending (default), <c>true</c> = ascending.
|
||||
/// </summary>
|
||||
public bool SortAsc { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,5 +9,25 @@ public class PersonCreateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the person.
|
||||
/// </summary>
|
||||
public required string Name {get; set;}
|
||||
}
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the asset ID to use as the profile picture.
|
||||
/// </summary>
|
||||
public Guid? ProfileAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropX { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropY { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the zoom level for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropZoom { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Defaults to <see cref="EVisibility.Protected"/>.
|
||||
/// </summary>
|
||||
public EVisibility Visibility { get; set; } = EVisibility.Protected;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,58 @@
|
||||
using Butter.Dtos.Album;
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the full details of a person, including associated assets and albums.
|
||||
/// Represents the full details of a person, including associated albums.
|
||||
/// </summary>
|
||||
public class PersonDetailedDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the person.
|
||||
/// </summary>
|
||||
public required Guid Id { get; set; }
|
||||
public required Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the person.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of asset IDs associated with this person.
|
||||
/// Gets or sets the asset ID used as the profile picture.
|
||||
/// </summary>
|
||||
public List<Guid>? Assets { get; set; }
|
||||
public Guid? ProfileAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of album IDs associated with this person.
|
||||
/// Gets or sets the horizontal crop offset for the profile picture (0–100).
|
||||
/// </summary>
|
||||
public List<Guid>? Albums { get; set; }
|
||||
public float? ProfileCropX { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical crop offset for the profile picture (0–100).
|
||||
/// </summary>
|
||||
public float? ProfileCropY { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the zoom level for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropZoom { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of non-deleted albums.
|
||||
/// </summary>
|
||||
public int TotalAlbums { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of non-deleted assets across all albums.
|
||||
/// </summary>
|
||||
public int TotalAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Only populated for curators and admins.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of album previews associated with this person.
|
||||
/// </summary>
|
||||
public List<AlbumPreviewDto>? Albums { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the IDs of users who maintain this person.
|
||||
/// </summary>
|
||||
public List<Guid>? MaintainerUserIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the usernames of users who maintain this person.
|
||||
/// </summary>
|
||||
public List<string>? MaintainerUsernames { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,9 +9,33 @@ public class PersonPreviewDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the person.
|
||||
/// </summary>
|
||||
public required Guid Id { get; set; }
|
||||
public required Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the person.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the asset ID used as the profile picture.
|
||||
/// </summary>
|
||||
public Guid? ProfileAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropX { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropY { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the zoom level for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropZoom { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of albums associated with this person.
|
||||
/// </summary>
|
||||
public int TotalAlbums { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level. Only populated for curators and admins.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
}
|
||||
|
||||
7
Butter/Dtos/Person/PersonSearchParametersDto.cs
Normal file
7
Butter/Dtos/Person/PersonSearchParametersDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
/// Search and filter parameters for person (cosplayer) queries.
|
||||
/// </summary>
|
||||
public class PersonSearchParametersDto : PagedSearchParametersDto {
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
@@ -7,6 +9,29 @@ public class PersonUpdateDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the person.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
//TODO: add the missing properties here
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the asset ID to use as the profile picture.
|
||||
/// </summary>
|
||||
public Guid? ProfileAssetId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropX { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical crop offset for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropY { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the zoom level for the profile picture.
|
||||
/// </summary>
|
||||
public float? ProfileCropZoom { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility level.
|
||||
/// </summary>
|
||||
public EVisibility? Visibility { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of user IDs that should be maintainers of this person (admin/curator only).
|
||||
/// </summary>
|
||||
public List<Guid>? MaintainerUserIds { get; set; }
|
||||
}
|
||||
|
||||
15
Butter/Dtos/Stats/MimeTypeStatDto.cs
Normal file
15
Butter/Dtos/Stats/MimeTypeStatDto.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Butter.Dtos.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// A MIME type and its associated asset count for statistics.
|
||||
/// </summary>
|
||||
public class MimeTypeStatDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type string.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets with this MIME type.
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
}
|
||||
23
Butter/Dtos/Stats/MonthlyStatDto.cs
Normal file
23
Butter/Dtos/Stats/MonthlyStatDto.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Butter.Dtos.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// Monthly growth statistics for assets and users.
|
||||
/// </summary>
|
||||
public class MonthlyStatDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
public int Year { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the month (1-12).
|
||||
/// </summary>
|
||||
public int Month { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of new assets added in this month.
|
||||
/// </summary>
|
||||
public int NewAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of new users registered in this month.
|
||||
/// </summary>
|
||||
public int NewUsers { get; set; }
|
||||
}
|
||||
15
Butter/Dtos/Stats/ResolutionBucketDto.cs
Normal file
15
Butter/Dtos/Stats/ResolutionBucketDto.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Butter.Dtos.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// A resolution bucket with a label and the count of assets that fall into it.
|
||||
/// </summary>
|
||||
public class ResolutionBucketDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the display label for the resolution bucket.
|
||||
/// </summary>
|
||||
public string Label { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets in this resolution bucket.
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
}
|
||||
146
Butter/Dtos/Stats/StatsDto.cs
Normal file
146
Butter/Dtos/Stats/StatsDto.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive statistics about the MilkyShots instance, gatherable from fast aggregate queries.
|
||||
/// </summary>
|
||||
public class StatsDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of assets (excluding soft-deleted).
|
||||
/// </summary>
|
||||
public int TotalAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of registered users (excluding soft-deleted).
|
||||
/// </summary>
|
||||
public int TotalUsers { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of albums.
|
||||
/// </summary>
|
||||
public int TotalAlbums { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of tags.
|
||||
/// </summary>
|
||||
public int TotalTags { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of people.
|
||||
/// </summary>
|
||||
public int TotalPeople { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of detected faces.
|
||||
/// </summary>
|
||||
public int TotalFaces { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of watched folders.
|
||||
/// </summary>
|
||||
public int TotalFolders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the asset count broken down by type (Image, Video, Animation, PhotoSphere).
|
||||
/// </summary>
|
||||
public Dictionary<EAssetType, int> AssetsByType { get; set; } = [];
|
||||
/// <summary>
|
||||
/// Gets or sets the total storage used by all assets in bytes.
|
||||
/// </summary>
|
||||
public long TotalStorageBytes { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the storage used broken down by asset type.
|
||||
/// </summary>
|
||||
public Dictionary<EAssetType, long> StorageByType { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user count broken down by access level.
|
||||
/// </summary>
|
||||
public Dictionary<EAccessLevel, int> UsersByAccessLevel { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets added in the last 7 days.
|
||||
/// </summary>
|
||||
public int AssetsAddedLast7Days { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets added in the last 30 days.
|
||||
/// </summary>
|
||||
public int AssetsAddedLast30Days { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of users registered in the last 30 days.
|
||||
/// </summary>
|
||||
public int UsersRegisteredLast30Days { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets with no associated folder.
|
||||
/// </summary>
|
||||
public int OrphanAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of publicly shared assets.
|
||||
/// </summary>
|
||||
public int PublicAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets with protected visibility (authenticated only).
|
||||
/// </summary>
|
||||
public int ProtectedAssets { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of private assets.
|
||||
/// </summary>
|
||||
public int PrivateAssets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of image assets missing resolution metadata.
|
||||
/// </summary>
|
||||
public int AssetsMissingMetadata { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets missing a generated thumbnail.
|
||||
/// </summary>
|
||||
public int AssetsMissingThumbnail { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets whose thumbnail was generated at a different size than the current setting.
|
||||
/// </summary>
|
||||
public int AssetsMissingThumbnailStale { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets missing a generated preview.
|
||||
/// </summary>
|
||||
public int AssetsMissingPreviews { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets whose preview was generated at a different size than the current setting.
|
||||
/// </summary>
|
||||
public int AssetsMissingPreviewsStale { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets missing a perceptual hash.
|
||||
/// </summary>
|
||||
public int AssetsMissingPhash { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets with no person assigned (no album with a PersonOwner).
|
||||
/// </summary>
|
||||
public int AssetsWithNoPerson { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets with no album assigned.
|
||||
/// </summary>
|
||||
public int AssetsWithNoAlbum { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of albums without a cover image.
|
||||
/// </summary>
|
||||
public int AlbumsMissingCover { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the number of people/cosplayers without a profile picture.
|
||||
/// </summary>
|
||||
public int CosplayersMissingProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the top tags by asset count.
|
||||
/// </summary>
|
||||
public List<TagStatDto> TopTags { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolution distribution of assets.
|
||||
/// </summary>
|
||||
public List<ResolutionBucketDto> ResolutionDistribution { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file format breakdown by MIME type.
|
||||
/// </summary>
|
||||
public List<MimeTypeStatDto> FileFormatBreakdown { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the monthly new-asset and new-user counts for the last 12 months.
|
||||
/// </summary>
|
||||
public List<MonthlyStatDto> MonthlyGrowth { get; set; } = [];
|
||||
}
|
||||
19
Butter/Dtos/Stats/TagStatDto.cs
Normal file
19
Butter/Dtos/Stats/TagStatDto.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Butter.Dtos.Stats;
|
||||
|
||||
/// <summary>
|
||||
/// A tag and its associated asset count for statistics.
|
||||
/// </summary>
|
||||
public class TagStatDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the tag identifier.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the tag name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the number of assets associated with the tag.
|
||||
/// </summary>
|
||||
public int AssetCount { get; set; }
|
||||
}
|
||||
@@ -46,4 +46,13 @@ public class UserInfoDto {
|
||||
/// Gets or sets the access level of the user.
|
||||
/// </summary>
|
||||
public required EAccessLevel AccessLevel { get; set; } = EAccessLevel.User;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IDs of persons (cosplayers) this user is a maintainer of.
|
||||
/// </summary>
|
||||
public List<Guid>? MaintainedPersonIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the names of persons (cosplayers) this user is a maintainer of.
|
||||
/// </summary>
|
||||
public List<string>? MaintainedPersonNames { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,12 +18,16 @@ public class UserUpdateDto {
|
||||
/// Gets or sets the new email address.
|
||||
/// </summary>
|
||||
public string? Email { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the new password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the user is banned.
|
||||
/// <summary>
|
||||
/// Gets or sets the new password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the current password (required for self-service password changes).
|
||||
/// </summary>
|
||||
public string? OldPassword { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the user is banned.
|
||||
/// </summary>
|
||||
public bool? IsBanned { get; set; }
|
||||
/// <summary>
|
||||
@@ -34,4 +38,8 @@ public class UserUpdateDto {
|
||||
/// Gets or sets the new access level.
|
||||
/// </summary>
|
||||
public EAccessLevel? AccessLevel { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the list of person (cosplayer) IDs this user should maintain (admin only).
|
||||
/// </summary>
|
||||
public List<Guid>? MaintainedPersonIds { get; set; }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ public enum Settings {
|
||||
/// <summary>Number of days to keep completed/failed/canceled jobs.</summary>
|
||||
JobRetentionDays,
|
||||
/// <summary>Number of assets to process per sub-job batch.</summary>
|
||||
JobBatchSize
|
||||
JobBatchSize,
|
||||
/// <summary>Quality setting (1–100) for WebP thumbnail encoding.</summary>
|
||||
ThumbnailQuality,
|
||||
/// <summary>Quality setting (1–100) for WebP preview encoding.</summary>
|
||||
PreviewQuality,
|
||||
/// <summary>User ID used for scanner-created asset attribution.</summary>
|
||||
SystemUploaderId
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,6 +59,9 @@ public static class SettingsExtensions {
|
||||
Settings.MaxConcurrentJobs => "Max Concurrent Jobs",
|
||||
Settings.JobRetentionDays => "Job Retention Days",
|
||||
Settings.JobBatchSize => "Job Batch Size",
|
||||
Settings.ThumbnailQuality => "Thumbnail Quality",
|
||||
Settings.PreviewQuality => "Preview Quality",
|
||||
Settings.SystemUploaderId => "System Uploader Id",
|
||||
_ => setting.ToString() // Fallback to the enum name
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ namespace Butter.Types;
|
||||
/// </summary>
|
||||
public enum EAccessLevel {
|
||||
/// <summary>Standard user with basic permissions.</summary>
|
||||
User,
|
||||
User = 0,
|
||||
/// <summary>Maintainer — scoped cosplayer management and tag management.</summary>
|
||||
Maintainer = 1,
|
||||
/// <summary>Curator with elevated permissions for managing assets.</summary>
|
||||
Curator,
|
||||
Curator = 2,
|
||||
/// <summary>Administrator with full system access.</summary>
|
||||
Admin
|
||||
Admin = 3
|
||||
}
|
||||
|
||||
13
Butter/Types/EVisibility.cs
Normal file
13
Butter/Types/EVisibility.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Butter.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Defines visibility levels for entities in the system.
|
||||
/// </summary>
|
||||
public enum EVisibility {
|
||||
/// <summary>Visible to everyone, including anonymous users.</summary>
|
||||
Public = 0,
|
||||
/// <summary>Visible to authenticated users only.</summary>
|
||||
Protected = 1,
|
||||
/// <summary>Visible only to authorized users (maintainers, curators, admins).</summary>
|
||||
Private = 2
|
||||
}
|
||||
23
Lactose.Analyzers/Lactose.Analyzers.csproj
Normal file
23
Lactose.Analyzers/Lactose.Analyzers.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
|
||||
<NoWarn>$(NoWarn);RS1038;RS2008</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.6.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
132
Lactose.Analyzers/OnlyUtcDateTimeAnalyzer.cs
Normal file
132
Lactose.Analyzers/OnlyUtcDateTimeAnalyzer.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Lactose.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class OnlyUtcDateTimeAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
const string Category = "Correctness";
|
||||
|
||||
static readonly DiagnosticDescriptor DateTimeNowRule = new(
|
||||
id: "MS001",
|
||||
title: "Use DateTime.UtcNow instead of DateTime.Now",
|
||||
messageFormat: "Use DateTime.UtcNow instead of DateTime.Now to ensure UTC semantics",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
static readonly DiagnosticDescriptor DateTimeTodayRule = new(
|
||||
id: "MS002",
|
||||
title: "Use DateTime.UtcNow.Date instead of DateTime.Today",
|
||||
messageFormat: "Use DateTime.UtcNow.Date instead of DateTime.Today to ensure UTC semantics",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
static readonly DiagnosticDescriptor DateTimeDateRule = new(
|
||||
id: "MS003",
|
||||
title: "DateTime.Date may return non-UTC value",
|
||||
messageFormat: "DateTime.Date produces a DateTime with Kind=Local; ensure this is intentional or use DateTime.UtcNow.Date",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
static readonly DiagnosticDescriptor SpecifyKindRule = new(
|
||||
id: "MS004",
|
||||
title: "DateTime.SpecifyKind must use DateTimeKind.Utc",
|
||||
messageFormat: "DateTime.SpecifyKind with {0} is not allowed; use DateTimeKind.Utc",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
||||
ImmutableArray.Create(DateTimeNowRule, DateTimeTodayRule, DateTimeDateRule, SpecifyKindRule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
|
||||
}
|
||||
|
||||
void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
|
||||
if (memberAccess.Name is not IdentifierNameSyntax memberName)
|
||||
return;
|
||||
|
||||
switch (memberName.Identifier.Text)
|
||||
{
|
||||
case "Now":
|
||||
if (IsSystemDateTime(context, memberAccess.Expression))
|
||||
context.ReportDiagnostic(Diagnostic.Create(DateTimeNowRule, memberAccess.GetLocation()));
|
||||
break;
|
||||
|
||||
case "Today":
|
||||
if (IsSystemDateTime(context, memberAccess.Expression))
|
||||
context.ReportDiagnostic(Diagnostic.Create(DateTimeTodayRule, memberAccess.GetLocation()));
|
||||
break;
|
||||
|
||||
case "Date":
|
||||
{
|
||||
var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression, context.CancellationToken);
|
||||
if (typeInfo.Type is INamedTypeSymbol namedType &&
|
||||
namedType.SpecialType == SpecialType.System_DateTime)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DateTimeDateRule, memberAccess.GetLocation()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var invocation = (InvocationExpressionSyntax)context.Node;
|
||||
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
||||
return;
|
||||
|
||||
if (memberAccess.Name is not IdentifierNameSyntax { Identifier.Text: "SpecifyKind" })
|
||||
return;
|
||||
|
||||
if (!IsSystemDateTime(context, memberAccess.Expression))
|
||||
return;
|
||||
|
||||
if (invocation.ArgumentList.Arguments.Count < 2)
|
||||
return;
|
||||
|
||||
var kindArg = invocation.ArgumentList.Arguments[1].Expression;
|
||||
var kindValue = context.SemanticModel.GetConstantValue(kindArg, context.CancellationToken);
|
||||
|
||||
if (!kindValue.HasValue || kindValue.Value is not int intKind)
|
||||
return;
|
||||
|
||||
var kindName = intKind switch
|
||||
{
|
||||
0 => "DateTimeKind.Unspecified",
|
||||
1 => "DateTimeKind.Utc",
|
||||
2 => "DateTimeKind.Local",
|
||||
_ => $"DateTimeKind value {intKind}"
|
||||
};
|
||||
|
||||
if (intKind != 1)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
SpecifyKindRule,
|
||||
kindArg.GetLocation(),
|
||||
kindName));
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsSystemDateTime(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
|
||||
{
|
||||
var typeInfo = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken);
|
||||
return typeInfo.Type is INamedTypeSymbol named &&
|
||||
named.SpecialType == SpecialType.System_DateTime;
|
||||
}
|
||||
}
|
||||
109
Lactose.Analyzers/OnlyUtcDateTimeCodeFix.cs
Normal file
109
Lactose.Analyzers/OnlyUtcDateTimeCodeFix.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Editing;
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Lactose.Analyzers;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OnlyUtcDateTimeCodeFix))]
|
||||
[Shared]
|
||||
public class OnlyUtcDateTimeCodeFix : CodeFixProvider
|
||||
{
|
||||
public override ImmutableArray<string> FixableDiagnosticIds =>
|
||||
ImmutableArray.Create("MS001", "MS002", "MS004");
|
||||
|
||||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
if (root is null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics[0];
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var node = root.FindNode(diagnosticSpan);
|
||||
|
||||
switch (diagnostic.Id)
|
||||
{
|
||||
case "MS001":
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: "Replace with DateTime.UtcNow",
|
||||
createChangedDocument: ct => ReplaceNowWithUtcNow(context.Document, node, ct),
|
||||
equivalenceKey: "MS001"),
|
||||
diagnostic);
|
||||
break;
|
||||
|
||||
case "MS002":
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: "Replace with DateTime.UtcNow.Date",
|
||||
createChangedDocument: ct => ReplaceTodayWithUtcNowDate(context.Document, node, ct),
|
||||
equivalenceKey: "MS002"),
|
||||
diagnostic);
|
||||
break;
|
||||
|
||||
case "MS004":
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: "Replace with DateTimeKind.Utc",
|
||||
createChangedDocument: ct => ReplaceWithUtcKind(context.Document, node, ct),
|
||||
equivalenceKey: "MS004"),
|
||||
diagnostic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<Document> ReplaceNowWithUtcNow(Document document, SyntaxNode node, CancellationToken ct)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
|
||||
if (node is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
var utcNow = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxFactory.IdentifierName("DateTime"),
|
||||
SyntaxFactory.IdentifierName("UtcNow"));
|
||||
editor.ReplaceNode(memberAccess, utcNow.WithTriviaFrom(memberAccess));
|
||||
}
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
|
||||
static async Task<Document> ReplaceTodayWithUtcNowDate(Document document, SyntaxNode node, CancellationToken ct)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
|
||||
if (node is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
var utcNow = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxFactory.IdentifierName("DateTime"),
|
||||
SyntaxFactory.IdentifierName("UtcNow"));
|
||||
var utcNowDotDate = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
utcNow,
|
||||
SyntaxFactory.IdentifierName("Date"));
|
||||
editor.ReplaceNode(memberAccess, utcNowDotDate.WithTriviaFrom(memberAccess));
|
||||
}
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
|
||||
static async Task<Document> ReplaceWithUtcKind(Document document, SyntaxNode node, CancellationToken ct)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
|
||||
if (node is ArgumentSyntax argument)
|
||||
{
|
||||
var utcKind = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxFactory.IdentifierName("DateTimeKind"),
|
||||
SyntaxFactory.IdentifierName("Utc"));
|
||||
editor.ReplaceNode(argument.Expression, utcKind.WithTriviaFrom(argument.Expression));
|
||||
}
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class RefreshTokenTransformation(
|
||||
if (userData == null) return Task.FromResult(principal);
|
||||
var user = userRepository.Find(userData.Id);
|
||||
if (user == null) return Task.FromResult(principal);
|
||||
if (string.IsNullOrEmpty(user.RefreshToken) || user.RefreshTokenExpires == null || user.RefreshTokenExpires < DateTime.Now) return Task.FromResult(principal);
|
||||
if (string.IsNullOrEmpty(user.RefreshToken) || user.RefreshTokenExpires == null || user.RefreshTokenExpires < DateTime.UtcNow) return Task.FromResult(principal);
|
||||
claimsIdentity.AddClaim(new Claim(ClaimType, "true", ClaimValueTypes.Boolean));
|
||||
principal.AddIdentity(claimsIdentity);
|
||||
return Task.FromResult(principal);
|
||||
|
||||
@@ -42,6 +42,10 @@ public class LactoseDbContext : DbContext {
|
||||
/// Gets or sets the job records DbSet.
|
||||
/// </summary>
|
||||
public DbSet<JobRecord> JobRecords { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the person-maintainer join table.
|
||||
/// </summary>
|
||||
public DbSet<PersonMaintainer> PersonMaintainers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="LactoseDbContext"/> class.
|
||||
@@ -49,32 +53,58 @@ public class LactoseDbContext : DbContext {
|
||||
/// <param name="options">The database context options.</param>
|
||||
public LactoseDbContext(DbContextOptions options) : base(options) {}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
|
||||
configurationBuilder.Properties<DateTime>()
|
||||
.HaveColumnType("timestamp with time zone");
|
||||
configurationBuilder.Properties<DateTime?>()
|
||||
.HaveColumnType("timestamp with time zone");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
modelBuilder.HasPostgresExtension("vector");
|
||||
modelBuilder.HasPostgresExtension("pg_trgm");
|
||||
//Album Relationships
|
||||
modelBuilder.Entity<Album>().HasOne(e => e.UserOwner).WithMany(e => e.OwnedAlbums);
|
||||
modelBuilder.Entity<Album>().HasOne(e => e.PersonOwner).WithMany(e => e.Albums);
|
||||
modelBuilder.Entity<Album>().HasOne(e => e.CoverAsset);
|
||||
modelBuilder.Entity<Album>().HasMany(e => e.Assets).WithMany(e => e.Albums);
|
||||
//Asset Relationships
|
||||
modelBuilder.Entity<Asset>().HasOne(e => e.Owner).WithMany(e => e.OwnedAssets);
|
||||
modelBuilder.Entity<Asset>().HasMany(e => e.SharedWith);
|
||||
modelBuilder.Entity<Asset>().HasOne(e => e.Uploader).WithMany(e => e.UploadedAssets).HasForeignKey(e => e.UploadedBy);
|
||||
modelBuilder.Entity<Asset>().HasMany(e => e.Albums).WithMany(e => e.Assets);
|
||||
modelBuilder.Entity<Asset>().HasMany(e => e.Tags).WithMany(e => e.Assets);
|
||||
modelBuilder.Entity<Asset>().HasMany(e => e.Faces).WithOne(e => e.Asset);
|
||||
modelBuilder.Entity<Asset>().HasOne(e => e.Folder).WithMany(e => e.Assets);
|
||||
modelBuilder.Entity<Asset>().HasIndex(e => new { e.Visibility, e.UploadedBy })
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
// People
|
||||
modelBuilder.Entity<Person>().HasIndex(p => p.Name)
|
||||
.HasDatabaseName("IX_People_Name_Trgm")
|
||||
.HasMethod("gin")
|
||||
.HasOperators("gin_trgm_ops");
|
||||
// Albums
|
||||
modelBuilder.Entity<Album>().HasIndex(a => a.Title)
|
||||
.HasDatabaseName("IX_Albums_Title_Trgm")
|
||||
.HasMethod("gin")
|
||||
.HasOperators("gin_trgm_ops");
|
||||
//Face Relationships
|
||||
modelBuilder.Entity<Face>().HasOne(e => e.Asset).WithMany(e => e.Faces);
|
||||
modelBuilder.Entity<Face>().HasOne(e => e.Person).WithMany(e => e.Faces);
|
||||
//Person Relationships
|
||||
modelBuilder.Entity<Person>().HasOne(e => e.ProfileAsset).WithMany().HasForeignKey(e => e.ProfileAssetId);
|
||||
modelBuilder.Entity<Person>().HasMany(e => e.Faces).WithOne(e => e.Person);
|
||||
modelBuilder.Entity<Person>().HasMany(e => e.Albums).WithOne(e => e.PersonOwner);
|
||||
modelBuilder.Entity<Person>().HasMany(e => e.Maintainers).WithMany(e => e.MaintainedPersons)
|
||||
.UsingEntity<PersonMaintainer>(
|
||||
j => j.HasOne(pm => pm.User).WithMany().HasForeignKey(pm => pm.UserId),
|
||||
j => j.HasOne(pm => pm.Person).WithMany().HasForeignKey(pm => pm.PersonId)
|
||||
);
|
||||
//Tag Relationships
|
||||
modelBuilder.Entity<Tag>().HasOne(e => e.Parent);
|
||||
modelBuilder.Entity<Tag>().HasMany(e => e.Assets).WithMany(e => e.Tags);
|
||||
//User Relationships
|
||||
modelBuilder.Entity<User>().HasMany(e => e.OwnedAssets).WithOne(e => e.Owner);
|
||||
modelBuilder.Entity<User>().HasMany(e => e.OwnedAlbums).WithOne(e => e.UserOwner);
|
||||
modelBuilder.Entity<User>().HasMany(e => e.UploadedAssets).WithOne(e => e.Uploader).HasForeignKey(e => e.UploadedBy);
|
||||
//Folder Relationships
|
||||
modelBuilder.Entity<Folder>().HasMany(e => e.Assets).WithOne(e => e.Folder);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Lactose.Controllers;
|
||||
/// <param name="authService">Authentication service.</param>
|
||||
/// <param name="albumRepository">Album repository.</param>
|
||||
/// <param name="assetRepository">Asset repository.</param>
|
||||
/// <param name="personRepository">Person repository.</param>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/[controller]")]
|
||||
@@ -24,7 +25,8 @@ public class AlbumController(
|
||||
//ILogger<AlbumController> logger,
|
||||
LactoseAuthService authService,
|
||||
IAlbumRepository albumRepository,
|
||||
IAssetRepository assetRepository
|
||||
IAssetRepository assetRepository,
|
||||
IPersonRepository personRepository
|
||||
) : ControllerBase {
|
||||
/// <summary>
|
||||
/// Gets an album by its ID with filtered assets based on access level.
|
||||
@@ -36,64 +38,39 @@ public class AlbumController(
|
||||
Guid? uid = authService.GetUserData(User)?.Id;
|
||||
EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
var album = albumRepository.Find(id);
|
||||
var album = albumRepository.FindVisible(id, uid, accessLevel);
|
||||
|
||||
// If the album does not exist, return a 404
|
||||
if (album == null) return NotFound();
|
||||
|
||||
// If the album has no assets, return an empty list
|
||||
if (album.Assets == null) return Ok(album.ToAlbumFullDto());
|
||||
|
||||
//Storage for assets that will be sent out;
|
||||
List<Asset> assets = new();
|
||||
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.User:
|
||||
// Only publicly shared assets or owned by user
|
||||
assets.AddRange(album.Assets.Where(asset => asset.IsPubliclyShared || asset.OwnerId == uid));
|
||||
// Add shared assets
|
||||
assets.AddRange(album.Assets.Where(asset => asset.SharedWith!.Any(user => user.Id == uid)));
|
||||
break;
|
||||
case EAccessLevel.Curator:
|
||||
case EAccessLevel.Admin:
|
||||
// All assets
|
||||
assets.AddRange(album.Assets);
|
||||
break;
|
||||
}
|
||||
|
||||
// changes the retrieved album to the new filtered
|
||||
album.Assets = assets;
|
||||
return Ok(album.ToAlbumFullDto());
|
||||
return Ok(album.ToAlbumFullDto(accessLevel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches albums with pagination and optional search term.
|
||||
/// Searches albums with pagination, optional search term, sort options, and optional unassigned filter.
|
||||
/// </summary>
|
||||
/// <param name="pagingOptions">Pagination and search parameters.</param>
|
||||
/// <param name="pagingOptions">Pagination, search, sort, and filter parameters.</param>
|
||||
/// <returns>A list of album previews accessible to the user.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<AlbumPreviewDto>> Search([FromQuery] PagedSearchParametersDto pagingOptions) {
|
||||
public ActionResult<List<AlbumPreviewDto>> Search([FromQuery] AlbumSearchParametersDto pagingOptions) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
if(pagingOptions.Page < 0 || pagingOptions.PageSize < 0) return BadRequest();
|
||||
|
||||
var albums = albumRepository.SearchQuery(pagingOptions.Search ?? string.Empty, pagingOptions.Page, pagingOptions.PageSize).ToList();
|
||||
|
||||
switch (accessLevel) {
|
||||
default:
|
||||
case EAccessLevel.User:
|
||||
List<Album> filteredAlbums;
|
||||
// Only publicly shared albums or owned by user
|
||||
filteredAlbums = albums.Where(album => album.Assets != null && (album.UserOwnerId == uid || album.Assets.Any(asset => asset.IsPubliclyShared))).ToList();
|
||||
// Add shared albums
|
||||
filteredAlbums.AddRange(albums.Where(album => album.Assets != null && album.Assets.Any(x => x.SharedWith!.Any(user => user.Id == uid))));
|
||||
return Ok(filteredAlbums.Select(x => x.ToAlbumPreviewDto()).ToList());
|
||||
case EAccessLevel.Curator:
|
||||
case EAccessLevel.Admin:
|
||||
// All albums
|
||||
return Ok(albums.Select(x => x.ToAlbumPreviewDto()).ToList());
|
||||
}
|
||||
|
||||
if(pagingOptions.Page < 0 || pagingOptions.PageSize < 1 || pagingOptions.PageSize > PagedParametersDto.MaxPageSize) return BadRequest();
|
||||
|
||||
var albums = albumRepository.SearchQuery(
|
||||
pagingOptions.Search ?? string.Empty,
|
||||
pagingOptions.Page,
|
||||
pagingOptions.PageSize,
|
||||
pagingOptions.SortBy,
|
||||
pagingOptions.SortAsc,
|
||||
pagingOptions.Unassigned,
|
||||
uid ?? default,
|
||||
accessLevel,
|
||||
pagingOptions.AssetUploadedBy
|
||||
).ToList();
|
||||
|
||||
return Ok(albums);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,7 +80,7 @@ public class AlbumController(
|
||||
/// <param name="albumDto">The album update data.</param>
|
||||
/// <returns>200 on success, 403 if forbidden, 404 if not found.</returns>
|
||||
[HttpPost("{id}")]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public ActionResult Update([FromRoute] Guid id, [FromBody] AlbumUpdateDto albumDto) {
|
||||
Guid? uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
@@ -113,28 +90,25 @@ public class AlbumController(
|
||||
// If the album does not exist, return a 404
|
||||
if (album == null) return NotFound();
|
||||
|
||||
// Maintainer scope check — must maintain the album's person
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.Maintainer when album.PersonOwnerId == null || !personRepository.IsMaintainerOf(uid!.Value, album.PersonOwnerId.Value):
|
||||
return Forbid();
|
||||
case EAccessLevel.User:
|
||||
// Users can't update albums
|
||||
return Forbid();
|
||||
case EAccessLevel.Curator:
|
||||
// Curators can only update albums they own
|
||||
if (album.UserOwnerId != uid) return Forbid();
|
||||
break;
|
||||
case EAccessLevel.Admin:
|
||||
// Admins can update any album
|
||||
break;
|
||||
}
|
||||
|
||||
// Update album properties
|
||||
album.Title = albumDto.Name ?? album.Title;
|
||||
album.UpdatedAt = DateTime.Now;
|
||||
album.PersonOwnerId = albumDto.Person ?? album.PersonOwnerId;
|
||||
album.UserOwnerId = albumDto.Owner ?? album.UserOwnerId;
|
||||
album.Title = string.IsNullOrEmpty(albumDto.Name) ? album.Title : albumDto.Name;
|
||||
album.UpdatedAt = DateTime.UtcNow;
|
||||
album.PersonOwnerId = albumDto.RemovePerson ? null : albumDto.Person ?? album.PersonOwnerId;
|
||||
album.Visibility = albumDto.Visibility ?? album.Visibility;
|
||||
album.Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : album.Assets;
|
||||
album.CoverAssetId = albumDto.CoverAssetId ?? album.CoverAssetId;
|
||||
|
||||
// Save changes
|
||||
albumRepository.Update(album);
|
||||
albumRepository.Save();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@@ -145,7 +119,7 @@ public class AlbumController(
|
||||
/// <param name="bulkDto">The bulk update data with album IDs and shared values.</param>
|
||||
/// <returns>200 on success, 403 if forbidden.</returns>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public ActionResult BulkUpdate([FromBody] BulkDto<AlbumUpdateDto> bulkDto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
@@ -156,9 +130,13 @@ public class AlbumController(
|
||||
case EAccessLevel.User:
|
||||
// Users can't update albums
|
||||
return Forbid();
|
||||
case EAccessLevel.Maintainer:
|
||||
// Maintainers can only update albums of persons they maintain
|
||||
albums = albums.Where(a => a.PersonOwnerId != null && personRepository.IsMaintainerOf(uid!.Value, a.PersonOwnerId.Value)).ToList();
|
||||
if (albums.Count == 0) return Forbid();
|
||||
break;
|
||||
case EAccessLevel.Curator:
|
||||
// Curators can only update albums they own
|
||||
albums = albums.Where(x => x.UserOwnerId == uid).ToList();
|
||||
// Curators can update any album
|
||||
break;
|
||||
case EAccessLevel.Admin:
|
||||
// Admins can update any album
|
||||
@@ -167,10 +145,11 @@ public class AlbumController(
|
||||
|
||||
foreach (var album in albums) {
|
||||
album.Title = bulkDto.Data.Name ?? album.Title;
|
||||
album.UpdatedAt = DateTime.Now;
|
||||
album.UpdatedAt = DateTime.UtcNow;
|
||||
album.PersonOwnerId = bulkDto.Data.Person ?? album.PersonOwnerId;
|
||||
album.UserOwnerId = bulkDto.Data.Owner ?? album.UserOwnerId;
|
||||
album.Visibility = bulkDto.Data.Visibility ?? album.Visibility;
|
||||
album.Assets = bulkDto.Data.Assets != null ? assetRepository.FindBulk(bulkDto.Data.Assets).ToList() : album.Assets;
|
||||
album.CoverAssetId = bulkDto.Data.CoverAssetId ?? album.CoverAssetId;
|
||||
}
|
||||
|
||||
albumRepository.UpdateBulk(albums);
|
||||
@@ -185,27 +164,26 @@ public class AlbumController(
|
||||
/// <param name="albumDto">The album creation data.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPut]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
public ActionResult Create([FromBody] AlbumCreateDto albumDto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
// If not admin, the owner of a new album is the current user
|
||||
albumDto.Owner = accessLevel == EAccessLevel.Admin ? albumDto.Owner : uid;
|
||||
var assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : new List<Asset>();
|
||||
|
||||
var album = new Album {
|
||||
Id = Guid.NewGuid(),
|
||||
Title = albumDto.Name,
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
PersonOwnerId = albumDto.Person ?? null,
|
||||
UserOwnerId = albumDto.Owner,
|
||||
Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : new List<Asset>()
|
||||
Visibility = albumDto.Visibility,
|
||||
CoverAssetId = albumDto.CoverAssetId ?? assets.OrderBy(a => a.OriginalFilename).FirstOrDefault()?.Id,
|
||||
Assets = assets
|
||||
};
|
||||
|
||||
albumRepository.Insert(album);
|
||||
albumRepository.Save();
|
||||
|
||||
return Ok();
|
||||
return Ok(album.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -214,18 +192,23 @@ public class AlbumController(
|
||||
/// <param name="albumIds">The list of album IDs to remove.</param>
|
||||
/// <returns>200 on success, 403 if forbidden.</returns>
|
||||
[HttpDelete]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public ActionResult BulkDelete([FromBody] List<Guid> albumIds) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
var albums = albumRepository.FindBulk(albumIds).ToList();
|
||||
|
||||
// If the user is not an admin, filter out albums they do not own
|
||||
if (accessLevel != EAccessLevel.Admin) { albums = albums.Where(x => x.UserOwnerId == uid).ToList(); }
|
||||
// Filter by access level
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.Maintainer:
|
||||
albums = albums.Where(a => a.PersonOwnerId != null && personRepository.IsMaintainerOf(uid!.Value, a.PersonOwnerId.Value)).ToList();
|
||||
break;
|
||||
case EAccessLevel.User:
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
// If no albums are left after filtering, return a 403
|
||||
if (albumIds.Count == 0) return Forbid();
|
||||
if (albums.Count == 0) return Forbid();
|
||||
|
||||
albums.ForEach(albumRepository.Remove);
|
||||
albumRepository.Save();
|
||||
@@ -239,7 +222,7 @@ public class AlbumController(
|
||||
/// <param name="id">The album ID.</param>
|
||||
/// <returns>200 on success, 404 if not found, 403 if forbidden.</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public ActionResult Delete(Guid id) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
@@ -248,10 +231,16 @@ public class AlbumController(
|
||||
|
||||
// If the album does not exist, return a 404
|
||||
if (album == null) return NotFound();
|
||||
// If the user is not the owner or an admin, return a 403
|
||||
if (accessLevel != EAccessLevel.Admin && album.UserOwnerId != uid) return Forbid();
|
||||
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.Maintainer when album.PersonOwnerId == null || !personRepository.IsMaintainerOf(uid!.Value, album.PersonOwnerId.Value):
|
||||
return Forbid();
|
||||
case EAccessLevel.User:
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
albumRepository.Remove(album);
|
||||
albumRepository.Save();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -18,13 +18,15 @@ namespace Lactose.Controllers;
|
||||
/// <param name="authService">Authentication service.</param>
|
||||
/// <param name="assetRepository">Asset repository.</param>
|
||||
/// <param name="userRepository">User repository.</param>
|
||||
/// <param name="personRepository">Person repository (for maintainer scope checks).</param>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AssetController(
|
||||
ILogger<AssetController> logger,
|
||||
LactoseAuthService authService,
|
||||
IAssetRepository assetRepository,
|
||||
IUserRepository userRepository
|
||||
IUserRepository userRepository,
|
||||
IPersonRepository personRepository
|
||||
) : ControllerBase {
|
||||
/// <summary>
|
||||
/// Gets an asset by its ID with full details, respecting access level.
|
||||
@@ -44,50 +46,14 @@ public class AssetController(
|
||||
"""
|
||||
);
|
||||
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = assetRepository.FindVisible(id, uid, accessLevel);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||
logger.LogWarning($"Request asset {id} not found or not visible!");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.User: {
|
||||
//Not publicly shared && Not shared with the user
|
||||
if (!asset.IsPubliclyShared && asset.SharedWith?.Find(x => x.Id == uid) == null) {
|
||||
logger.LogTrace($"{asset.Id} is not shared with user {uid}");
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
//Deleted asset
|
||||
if (asset.DeletedAt != null) {
|
||||
logger.LogTrace($"{asset.Id} is deleted and thus not exists for this user");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EAccessLevel.Curator: {
|
||||
//Deleted asset owned by the user
|
||||
if (asset.DeletedAt != null)
|
||||
if (asset.Owner?.Id != uid) {
|
||||
logger.LogTrace(
|
||||
$"{asset.Id} is deleted and it's not owned by this user so it does not exists for this user"
|
||||
);
|
||||
|
||||
return NotFound();
|
||||
} else { logger.LogTrace($"{asset.Id} is deleted but it's owned by this user so it exists for this user"); }
|
||||
|
||||
break;
|
||||
}
|
||||
case EAccessLevel.Admin: {
|
||||
//Admins will see this regardless
|
||||
logger.LogTrace($"{asset.Id} is being requested by an admin, sending it regardless of the visibility state");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AssetDto dto = asset.ToFullAssetsDto(accessLevel);
|
||||
AssetDto dto = asset.ToFullAssetsDto(accessLevel, uid);
|
||||
|
||||
logger.LogTrace(
|
||||
$"""
|
||||
@@ -95,11 +61,11 @@ public class AssetController(
|
||||
dto.Id: {dto.Id}
|
||||
dto.FileName: {dto.FileName}
|
||||
dto.AssetType: {dto.AssetType}
|
||||
dto.IsPublic: {dto.IsPublic}
|
||||
dto.Visibility: {dto.Visibility}
|
||||
dto.CreatedAt: {dto.CreatedAt}
|
||||
dto.UpdatedAt: {dto.UpdatedAt}
|
||||
dto.DeletedAt: {dto.DeletedAt}
|
||||
dto.Owner: {dto.Owner.Id} {dto.Owner.Username}
|
||||
dto.UploadedBy: {dto.UploadedBy.Id} {dto.UploadedBy.Username}
|
||||
"""
|
||||
);
|
||||
|
||||
@@ -107,55 +73,51 @@ public class AssetController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches assets by date range with pagination.
|
||||
/// Searches assets with optional date range, type filter, and random ordering with pagination.
|
||||
/// </summary>
|
||||
/// <param name="searchOptionsDto">Search options including date range and pagination.</param>
|
||||
/// <param name="searchOptionsDto">Search options including date range, type filter, random ordering, and pagination.</param>
|
||||
/// <returns>A list of asset previews.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<AssetPreviewDto>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
DateTime from = DateTime.Parse(searchOptionsDto.StartDate ?? string.Empty);
|
||||
DateTime to = DateTime.Parse(searchOptionsDto.EndDate ?? string.Empty);
|
||||
var userData = authService.GetUserData(User);
|
||||
var uid = userData?.Id;
|
||||
EAccessLevel accessLevel = userData?.AccessLevel ?? EAccessLevel.User;
|
||||
DateTime? from = string.IsNullOrEmpty(searchOptionsDto.StartDate) ? null : DateTime.Parse(searchOptionsDto.StartDate);
|
||||
DateTime? to = string.IsNullOrEmpty(searchOptionsDto.EndDate) ? null : DateTime.Parse(searchOptionsDto.EndDate);
|
||||
|
||||
if (from > to) {
|
||||
if (from.HasValue && to.HasValue && from > to) {
|
||||
logger.LogWarning("Invalid date range provided");
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (searchOptionsDto.Page < 1 || searchOptionsDto.PageSize < 1) {
|
||||
if (searchOptionsDto.Page < 0 || searchOptionsDto.PageSize < 1 || searchOptionsDto.PageSize > PagedParametersDto.MaxPageSize) {
|
||||
logger.LogWarning("Invalid pagination parameters provided");
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var assets = assetRepository.FindByDateRange(from, to, searchOptionsDto.Page, searchOptionsDto.PageSize);
|
||||
var assets = assetRepository.GetAssets(
|
||||
searchOptionsDto.Type, from, to, searchOptionsDto.Random, searchOptionsDto.Seed,
|
||||
searchOptionsDto.Page, searchOptionsDto.PageSize, out int total,
|
||||
uid, accessLevel
|
||||
);
|
||||
|
||||
logger.LogTrace(
|
||||
$"""
|
||||
Requested assets
|
||||
by user: {uid}
|
||||
by user: {uid}
|
||||
access level: {accessLevel}
|
||||
type: {searchOptionsDto.Type}
|
||||
random: {searchOptionsDto.Random}
|
||||
date range: {from} - {to}
|
||||
"""
|
||||
page: {searchOptionsDto.Page}
|
||||
"""
|
||||
);
|
||||
|
||||
switch (accessLevel) {
|
||||
case EAccessLevel.User: {
|
||||
//Only publicly shared assets and assets shared with the user that are not deleted
|
||||
assets = assets.Where(
|
||||
a => a.IsPubliclyShared || a.SharedWith?.Find(x => x.Id == uid) != null && a.DeletedAt != null
|
||||
);
|
||||
var dtoList = assets.ToAssetPreviewDto(accessLevel).ToList();
|
||||
|
||||
break;
|
||||
}
|
||||
case EAccessLevel.Curator:
|
||||
case EAccessLevel.Admin: {
|
||||
//Admins will see this regardless
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(assets.ToAssetPreviewDto());
|
||||
logger.LogTrace($"Returning {dtoList.Count} assets (total: {total})");
|
||||
Response.Headers["X-Total-Count"] = total.ToString();
|
||||
return Ok(dtoList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -165,32 +127,35 @@ public class AssetController(
|
||||
/// <param name="dto">The asset update data.</param>
|
||||
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
||||
[HttpPost("{id}")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] AssetUpdateDto dto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = assetRepository.FindWithAlbums(id);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (asset.Owner?.Id != uid && accesslevel != EAccessLevel.Admin) {
|
||||
if (asset.UploadedBy != uid && accesslevel != EAccessLevel.Admin && accesslevel != EAccessLevel.Curator
|
||||
&& (accesslevel != EAccessLevel.Maintainer || !uid.HasValue || !IsMaintainedByUser(uid.Value, asset))) {
|
||||
logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to update asset {id} that they do not own");
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
string log = "";
|
||||
|
||||
asset.IsPubliclyShared = dto.IsPublic ?? asset.IsPubliclyShared;
|
||||
log += $"PubliclyShared: {asset.IsPubliclyShared} -> {dto.IsPublic ?? asset.IsPubliclyShared}\n";
|
||||
asset.Visibility = dto.Visibility ?? asset.Visibility;
|
||||
log += $"Visibility: {asset.Visibility} -> {dto.Visibility ?? asset.Visibility}\n";
|
||||
|
||||
asset.DeletedAt = dto.DeletedAt ?? asset.DeletedAt;
|
||||
log += $"DeletedAt: {asset.DeletedAt} -> {dto.DeletedAt ?? asset.DeletedAt}\n";
|
||||
|
||||
asset.Owner = dto.Owner != null ? userRepository.Find(dto.Owner.Value) : asset.Owner;
|
||||
log += $"Owner: {asset.Owner?.Id} -> {dto.Owner ?? asset.Owner?.Id}\n";
|
||||
if (accesslevel == EAccessLevel.Admin) {
|
||||
asset.UploadedBy = dto.UploadedBy ?? asset.UploadedBy;
|
||||
log += $"UploadedBy: {asset.UploadedBy} -> {dto.UploadedBy ?? asset.UploadedBy}\n";
|
||||
}
|
||||
|
||||
logger.LogTrace($"Updating asset {id}\n{log}");
|
||||
|
||||
@@ -206,7 +171,7 @@ public class AssetController(
|
||||
/// <param name="bulkDto">The bulk update data with asset IDs and shared values.</param>
|
||||
/// <returns>200 on success, or an error response.</returns>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<AssetUpdateDto> bulkDto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
@@ -225,13 +190,19 @@ public class AssetController(
|
||||
}
|
||||
|
||||
//If the user is not an admin, they can only update their own assets
|
||||
if (accesslevel != EAccessLevel.Admin) enumerable = enumerable.Where(x => x.OwnerId == uid).ToList();
|
||||
if (accesslevel != EAccessLevel.Admin && uid.HasValue)
|
||||
enumerable = accesslevel == EAccessLevel.Maintainer
|
||||
? enumerable.Where(x => x.UploadedBy == uid || IsMaintainedByUser(uid.Value, x)).ToList()
|
||||
: enumerable.Where(x => x.UploadedBy == uid).ToList();
|
||||
|
||||
enumerable.ForEach(
|
||||
x => {
|
||||
x.IsPubliclyShared = bulkDto.Data.IsPublic ?? x.IsPubliclyShared;
|
||||
x.DeletedAt = bulkDto.Data.DeletedAt ?? x.DeletedAt;
|
||||
x.Owner = bulkDto.Data.Owner != null ? userRepository.Find(bulkDto.Data.Owner.Value) : x.Owner;
|
||||
x.Visibility = bulkDto.Data.Visibility ?? x.Visibility;
|
||||
x.DeletedAt = bulkDto.Data.DeletedAt ?? x.DeletedAt;
|
||||
|
||||
if (accesslevel == EAccessLevel.Admin) {
|
||||
x.UploadedBy = bulkDto.Data.UploadedBy ?? x.UploadedBy;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -245,23 +216,24 @@ public class AssetController(
|
||||
/// <param name="id">The asset ID.</param>
|
||||
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public IStatusCodeActionResult Delete([FromRoute] Guid id) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
var asset = assetRepository.Find(id);
|
||||
var asset = assetRepository.FindWithAlbums(id);
|
||||
|
||||
if (asset == null) {
|
||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (asset.Owner?.Id != uid && accesslevel != EAccessLevel.Admin) {
|
||||
if (asset.UploadedBy != uid && accesslevel != EAccessLevel.Admin && accesslevel != EAccessLevel.Curator
|
||||
&& (accesslevel != EAccessLevel.Maintainer || !uid.HasValue || !IsMaintainedByUser(uid.Value, asset))) {
|
||||
logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to delete asset {id} that they do not own");
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
asset.DeletedAt = DateTime.Now;
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
assetRepository.Update(asset);
|
||||
assetRepository.Save();
|
||||
|
||||
@@ -274,7 +246,7 @@ public class AssetController(
|
||||
/// <param name="ids">The list of asset IDs to delete.</param>
|
||||
/// <returns>200 on success, or an error response.</returns>
|
||||
[HttpDelete]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public IStatusCodeActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
@@ -293,9 +265,12 @@ public class AssetController(
|
||||
}
|
||||
|
||||
//If the user is not an admin, they can only delete their own assets
|
||||
if (accesslevel != EAccessLevel.Admin) enumerable = enumerable.Where(x => x.OwnerId == uid).ToList();
|
||||
if (accesslevel != EAccessLevel.Admin && uid.HasValue)
|
||||
enumerable = accesslevel == EAccessLevel.Maintainer
|
||||
? enumerable.Where(x => x.UploadedBy == uid || IsMaintainedByUser(uid.Value, x)).ToList()
|
||||
: enumerable.Where(x => x.UploadedBy == uid).ToList();
|
||||
|
||||
enumerable.ForEach(x => x.DeletedAt = DateTime.Now);
|
||||
enumerable.ForEach(x => x.DeletedAt = DateTime.UtcNow);
|
||||
|
||||
assetRepository.UpdateBulk(enumerable);
|
||||
return Ok();
|
||||
@@ -313,23 +288,22 @@ public class AssetController(
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||
|
||||
if (userRepository.Find(dto.Owner) == null) {
|
||||
logger.LogWarning($"User {dto.Owner} does not exist in the database");
|
||||
return BadRequest();
|
||||
}
|
||||
if (dto.UploadedBy.HasValue) {
|
||||
if (userRepository.Find(dto.UploadedBy.Value) == null) {
|
||||
logger.LogWarning($"User {dto.UploadedBy} does not exist in the database");
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (accesslevel != EAccessLevel.Admin && dto.Owner != uid) {
|
||||
logger.LogWarning($"User {uid} is trying to create an asset for user {dto.Owner}");
|
||||
return Unauthorized();
|
||||
if (accesslevel != EAccessLevel.Admin && dto.UploadedBy != uid) {
|
||||
logger.LogWarning($"User {uid} is trying to create an asset for user {dto.UploadedBy}");
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: This makes exactly zero sense. How are we supposed to create an asset externally here?
|
||||
// Does this even need to exist?
|
||||
var asset = new Asset {
|
||||
Id = Guid.NewGuid(),
|
||||
Type = dto.AssetType,
|
||||
IsPubliclyShared = dto.IsPublic,
|
||||
OwnerId = dto.Owner,
|
||||
Visibility = dto.Visibility,
|
||||
UploadedBy = dto.UploadedBy,
|
||||
CreatedAt = dto.CreatedAt,
|
||||
MimeType = dto.MimeType,
|
||||
ResolutionWidth = dto.ResolutionWidth,
|
||||
@@ -346,4 +320,14 @@ public class AssetController(
|
||||
|
||||
return Ok(asset.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a Maintainer has access to an asset through the cosplayers they maintain.
|
||||
/// A Maintainer can access assets that are in any album belonging to a person they maintain.
|
||||
/// </summary>
|
||||
/// <param name="uid">The maintainer's user ID.</param>
|
||||
/// <param name="asset">The asset to check.</param>
|
||||
/// <returns><see langword="true"/> if the maintainer is linked via album→person→maintainer chain.</returns>
|
||||
private bool IsMaintainedByUser(Guid uid, Asset asset) =>
|
||||
asset.Albums?.Any(a => a.PersonOwnerId.HasValue && personRepository.IsMaintainerOf(uid, a.PersonOwnerId.Value)) == true;
|
||||
}
|
||||
|
||||
@@ -50,19 +50,23 @@ public class AuthController(
|
||||
);
|
||||
}
|
||||
if (user.BannedAt != null) {
|
||||
return StatusCode(StatusCodes.Status100Continue,new AuthResultDto() {
|
||||
return StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is banned"
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (user.DeletedAt != null) {
|
||||
return StatusCode(StatusCodes.Status100Continue,new AuthResultDto() {
|
||||
return StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is disabled"
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
PasswordVerificationResult result = passwordHasher.VerifyHashedPassword(user, user.Password, userDto.Password);
|
||||
|
||||
@@ -75,7 +79,7 @@ public class AuthController(
|
||||
case PasswordVerificationResult.Success: break;
|
||||
}
|
||||
|
||||
user.LastLogin = DateTime.Now;
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
|
||||
var token = authService.GenerateAccessToken(user);
|
||||
var refreshToken = authService.GenerateRefreshToken(user);
|
||||
@@ -112,8 +116,8 @@ public class AuthController(
|
||||
Username = dto.Username,
|
||||
Email = dto.Email,
|
||||
AccessLevel = EAccessLevel.User,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
user.Password = passwordHasher.HashPassword(user, dto.Password);
|
||||
@@ -123,19 +127,6 @@ public class AuthController(
|
||||
return Ok(user.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the current authentication status and returns the authenticated user data.
|
||||
/// </summary>
|
||||
/// <returns>The authenticated user's data.</returns>
|
||||
[Authorize]
|
||||
[HttpPost()]
|
||||
public ActionResult<LactoseAuthenticatedUser> Check() {
|
||||
// this.User;
|
||||
//ClaimsPrincipal principal = User;
|
||||
LactoseAuthenticatedUser? identity = authService.GetUserData(User);
|
||||
return Ok(identity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs out the user by invalidating their refresh token.
|
||||
/// </summary>
|
||||
@@ -178,7 +169,7 @@ public class AuthController(
|
||||
|
||||
if (user.BannedAt != null) {
|
||||
return StatusCode(
|
||||
StatusCodes.Status100Continue,
|
||||
StatusCodes.Status403Forbidden,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is banned"
|
||||
@@ -188,7 +179,7 @@ public class AuthController(
|
||||
|
||||
if (user.DeletedAt != null) {
|
||||
return StatusCode(
|
||||
StatusCodes.Status100Continue,
|
||||
StatusCodes.Status403Forbidden,
|
||||
new AuthResultDto() {
|
||||
Success = false,
|
||||
ErrorMessage = "User is disabled"
|
||||
@@ -210,11 +201,11 @@ public class AuthController(
|
||||
Token = authService.GenerateAccessToken(user)
|
||||
};
|
||||
|
||||
if ((user.RefreshTokenExpires - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) < DateTime.Now) {
|
||||
if ((user.RefreshTokenExpires - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) < DateTime.UtcNow) {
|
||||
var refreshToken = authService.GenerateRefreshToken(user);
|
||||
authDto.RefreshToken = refreshToken;
|
||||
user.RefreshToken = refreshToken;
|
||||
user.RefreshTokenExpires = DateTime.Now.AddMinutes(LactoseAuthService.LongExpirationTime);
|
||||
user.RefreshTokenExpires = DateTime.UtcNow.AddMinutes(LactoseAuthService.LongExpirationTime);
|
||||
userRepository.Save();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Butter.Dtos;
|
||||
using Butter.Dtos.Jobs;
|
||||
using Butter.Types;
|
||||
using Lactose.Jobs;
|
||||
using Lactose.Mapper;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -35,7 +35,8 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
/// </summary>
|
||||
[HttpGet("past")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult<PastJobsResponse> GetPast([FromQuery] int page = 1, [FromQuery] int pageSize = 5) {
|
||||
public ActionResult<PastJobsResponse> GetPast([FromQuery] int page = 0, [FromQuery] int pageSize = 5) {
|
||||
if (page < 0 || pageSize < 1 || pageSize > PagedParametersDto.MaxPageSize) { return BadRequest(); }
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
var (jobs, total) = jobManager.GetPastRootJobs(page, pageSize);
|
||||
@@ -44,13 +45,16 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
|
||||
/// <summary>
|
||||
/// Returns children of a specific parent job (active + past).
|
||||
/// Supports delta sync — pass <paramref name="since"/> to receive only children changed after that timestamp.
|
||||
/// The <see cref="ChildrenResponse.Since"/> field contains the timestamp to use for the next delta poll.
|
||||
/// </summary>
|
||||
[HttpGet("{parentId}/children")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult<List<JobStatusDto>> GetChildren(Guid parentId) {
|
||||
public ActionResult<ChildrenResponse> GetChildren(Guid parentId, [FromQuery] DateTime? since) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
return Ok(jobManager.GetChildren(parentId));
|
||||
var children = jobManager.GetChildren(parentId, since);
|
||||
return Ok(new ChildrenResponse { Children = children, Since = DateTime.UtcNow });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,8 +119,7 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
if (jobRequest.Parameters.Count != 0)
|
||||
return BadRequest("PreviewGeneration job does not require any parameters.");
|
||||
|
||||
try { jobScheduler.QueuePreviewGeneration(null); }
|
||||
catch (NotImplementedException) { return StatusCode(501, "PreviewGeneration is not yet implemented."); }
|
||||
jobScheduler.QueuePreviewGeneration(null);
|
||||
return Ok();
|
||||
case EJobType.MetadataExtraction:
|
||||
if (jobRequest.Parameters.Count != 0)
|
||||
@@ -130,6 +133,16 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
||||
|
||||
jobScheduler.QueueIntegrityCheck(null);
|
||||
return Ok();
|
||||
case EJobType.CreatePersons:
|
||||
if (jobRequest.Parameters.Count != 0)
|
||||
return BadRequest("CreatePersons job does not require any parameters.");
|
||||
jobScheduler.QueueCreatePersons(null);
|
||||
return Ok();
|
||||
case EJobType.CreateAlbums:
|
||||
if (jobRequest.Parameters.Count != 0)
|
||||
return BadRequest("CreateAlbums job does not require any parameters.");
|
||||
jobScheduler.QueueCreateAlbums(null);
|
||||
return Ok();
|
||||
case EJobType.PHashGeneration:
|
||||
if (jobRequest.Parameters.Count > 1)
|
||||
return BadRequest("PHashGeneration job takes zero or one parameter (the asset ID).");
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using Butter.Types;
|
||||
using Lactose.Configuration;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace Lactose.Controllers;
|
||||
|
||||
@@ -10,17 +15,19 @@ namespace Lactose.Controllers;
|
||||
/// </summary>
|
||||
/// <param name="authService">Authentication service.</param>
|
||||
/// <param name="mediaRepository">Media repository.</param>
|
||||
/// <param name="assetRepository">Asset repository.</param>
|
||||
/// <param name="signKeyOptions">JWT signing key configuration.</param>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[AllowAnonymous]
|
||||
[Route("api/[controller]")]
|
||||
public class MediaController(
|
||||
//TODO: Implement logging
|
||||
//ILogger<MediaController> logger
|
||||
LactoseAuthService authService,
|
||||
IMediaRepository mediaRepository
|
||||
IMediaRepository mediaRepository,
|
||||
IAssetRepository assetRepository,
|
||||
IOptions<SignKeyConfiguration> signKeyOptions
|
||||
) : ControllerBase {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original image file for an asset.
|
||||
/// </summary>
|
||||
@@ -29,9 +36,15 @@ public class MediaController(
|
||||
[HttpGet("{id}")]
|
||||
[HttpGet("original/{id}")]
|
||||
public ActionResult GetImage(Guid id) {
|
||||
var user = authService.GetUserData(User);
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
var data = mediaRepository.GetOriginalData(id, user?.Id);
|
||||
if (CanAccessAssetDirectly(user, asset.DeletedAt)) {
|
||||
return PhysicalFile(asset.OriginalPath, asset.MimeType);
|
||||
}
|
||||
var data = mediaRepository.GetOriginalData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||
if (data == null) return NotFound();
|
||||
|
||||
return PhysicalFile(data.Path, data.MimeType);
|
||||
@@ -44,9 +57,16 @@ public class MediaController(
|
||||
/// <returns>The physical thumbnail file, or 404 if not found or access denied.</returns>
|
||||
[HttpGet("thumb/{id}")]
|
||||
public ActionResult GetThumb(Guid id) {
|
||||
var user = authService.GetUserData(User);
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
var data = mediaRepository.GetThumbData(id, user?.Id);
|
||||
if (CanAccessAssetDirectly(user, asset.DeletedAt)) {
|
||||
if (string.IsNullOrEmpty(asset.ThumbnailPath)) return NotFound();
|
||||
return PhysicalFile(asset.ThumbnailPath, "image/webp");
|
||||
}
|
||||
var data = mediaRepository.GetThumbData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||
if (data == null) return NotFound();
|
||||
|
||||
return PhysicalFile(data.Path, data.MimeType);
|
||||
@@ -59,11 +79,58 @@ public class MediaController(
|
||||
/// <returns>The physical preview file, or 404 if not found or access denied.</returns>
|
||||
[HttpGet("preview/{id}")]
|
||||
public ActionResult GetPreview(Guid id) {
|
||||
var user = authService.GetUserData(User);
|
||||
var user = GetUserFromRequest();
|
||||
var asset = assetRepository.Find(id);
|
||||
|
||||
if (asset == null) return NotFound();
|
||||
|
||||
var data = mediaRepository.GetPreviewData(id, user?.Id);
|
||||
if (CanAccessAssetDirectly(user, asset.DeletedAt)) {
|
||||
if (string.IsNullOrEmpty(asset.PreviewPath)) return NotFound();
|
||||
return PhysicalFile(asset.PreviewPath, "image/webp");
|
||||
}
|
||||
var data = mediaRepository.GetPreviewData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||
if (data == null) return NotFound();
|
||||
|
||||
return PhysicalFile(data.Path, data.MimeType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the authenticated user from the request, first trying the Authorization header,
|
||||
/// then falling back to the <c>token</c> query parameter for browser image requests.
|
||||
/// </summary>
|
||||
private LactoseAuthenticatedUser? GetUserFromRequest() {
|
||||
var user = authService.GetUserData(User);
|
||||
if (user != null) return user;
|
||||
|
||||
var token = Request.Query["token"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(token)) return null;
|
||||
|
||||
try {
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var key = signKeyOptions.Value.GetSecurityKey();
|
||||
var principal = handler.ValidateToken(token, new TokenValidationParameters {
|
||||
IssuerSigningKey = key,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
}, out _);
|
||||
return authService.GetUserData(principal);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the requester can access asset files directly without repository-level filtering.
|
||||
/// </summary>
|
||||
/// <param name="user">The authenticated user, if any.</param>
|
||||
/// <param name="deletedAt">The asset deletion timestamp.</param>
|
||||
/// <returns><see langword="true"/> for admins, or for curators when the asset is not deleted; otherwise <see langword="false"/>.</returns>
|
||||
private static bool CanAccessAssetDirectly(LactoseAuthenticatedUser? user, DateTime? deletedAt) {
|
||||
return user?.AccessLevel switch {
|
||||
EAccessLevel.Admin => true,
|
||||
EAccessLevel.Curator => deletedAt == null,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,71 @@
|
||||
using Butter.Dtos;
|
||||
using Butter.Dtos.Person;
|
||||
using Butter.Types;
|
||||
using Lactose.Mapper;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Lactose.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages people (faces/individuals identified in assets). Not yet implemented.
|
||||
/// Manages people (faces/individuals identified in assets).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/[controller]")]
|
||||
public class PersonController : ControllerBase {
|
||||
public class PersonController(
|
||||
IPersonRepository personRepository,
|
||||
IAlbumRepository albumRepository,
|
||||
LactoseAuthService authService
|
||||
) : ControllerBase {
|
||||
/// <summary>
|
||||
/// Gets a person by ID with optional associated assets and albums.
|
||||
/// Gets a person by ID with their associated albums, filtered by access level.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets a person by ID with their associated albums, filtered by access level and paginated.
|
||||
/// </summary>
|
||||
/// <param name="id">The person ID.</param>
|
||||
/// <param name="assets">Maximum number of associated assets to include.</param>
|
||||
/// <param name="albums">Maximum number of associated albums to include.</param>
|
||||
/// <returns>The person details.</returns>
|
||||
/// <param name="albumPaging">Pagination, search, and sort parameters for albums.</param>
|
||||
/// <returns>The person details with albums, or 404 if not found.</returns>
|
||||
[HttpGet("{id}")]
|
||||
public ActionResult<PersonDetailedDto> Get([FromRoute] Guid id, [FromRoute] uint? assets=100, [FromRoute] uint? albums=100) => throw new NotImplementedException();
|
||||
|
||||
public ActionResult<PersonDetailedDto> Get([FromRoute] Guid id, [FromQuery] PagedSearchParametersDto albumPaging) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
var person = personRepository.FindVisible(id, uid, accessLevel,
|
||||
albumPaging.Search, albumPaging.SortBy, albumPaging.SortAsc,
|
||||
albumPaging.Page, albumPaging.PageSize);
|
||||
if (person == null) return NotFound();
|
||||
|
||||
return Ok(person.ToPersonDetailedDto(accessLevel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches people with pagination.
|
||||
/// Searches people with pagination, optional search term, sort options, filtering out cosplayers with no visible content for non-admin users.
|
||||
/// </summary>
|
||||
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
||||
/// <returns>A list of people previews.</returns>
|
||||
/// <param name="pagedSearch">Pagination, search, and sort parameters.</param>
|
||||
/// <returns>A paginated list of person previews.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<PersonPreviewDto>> GetAll(PagedSearchParametersDto pagedSearch) => throw new NotImplementedException();
|
||||
|
||||
public ActionResult<List<PersonPreviewDto>> GetAll([FromQuery] PersonSearchParametersDto pagedSearch) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
if (pagedSearch.Page < 0 || pagedSearch.PageSize < 1 || pagedSearch.PageSize > PagedParametersDto.MaxPageSize) { return BadRequest(); }
|
||||
|
||||
var people = personRepository.SearchQuery(
|
||||
pagedSearch.Search ?? string.Empty,
|
||||
pagedSearch.Page,
|
||||
pagedSearch.PageSize,
|
||||
pagedSearch.SortBy,
|
||||
pagedSearch.SortAsc,
|
||||
uid ?? default,
|
||||
accessLevel
|
||||
).ToList();
|
||||
return Ok(people);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a person.
|
||||
/// </summary>
|
||||
@@ -38,8 +73,37 @@ public class PersonController : ControllerBase {
|
||||
/// <param name="dto">The updated person data.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPost("{id}")]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] PersonUpdateDto dto) =>throw new NotImplementedException();
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
public IActionResult Update([FromRoute] Guid id, [FromBody] PersonUpdateDto dto) {
|
||||
var uid = authService.GetUserData(User)?.Id;
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
var person = personRepository.Find(id);
|
||||
if (person == null) return NotFound();
|
||||
|
||||
if (accessLevel == EAccessLevel.Maintainer) {
|
||||
if (uid == null || !personRepository.IsMaintainerOf(uid.Value, id))
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
person.Name = dto.Name;
|
||||
person.ProfileAssetId = dto.ProfileAssetId;
|
||||
person.ProfileCropX = dto.ProfileCropX;
|
||||
person.ProfileCropY = dto.ProfileCropY;
|
||||
person.ProfileCropZoom = dto.ProfileCropZoom;
|
||||
|
||||
if (dto.Visibility != null && accessLevel >= EAccessLevel.Maintainer)
|
||||
person.Visibility = dto.Visibility.Value;
|
||||
|
||||
if (dto.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
|
||||
personRepository.SetMaintainers(person.Id, dto.MaintainerUserIds);
|
||||
|
||||
person.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
personRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates multiple people in a bulk operation.
|
||||
/// </summary>
|
||||
@@ -47,25 +111,75 @@ public class PersonController : ControllerBase {
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IStatusCodeActionResult BulkUpdate([FromBody]BulkDto<PersonUpdateDto> personBulkDto)=>throw new NotImplementedException();
|
||||
public IActionResult BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
|
||||
var people = personRepository.FindBulk(personBulkDto.Ids).ToList();
|
||||
|
||||
foreach (var person in people) {
|
||||
person.Name = personBulkDto.Data.Name;
|
||||
person.ProfileAssetId = personBulkDto.Data.ProfileAssetId;
|
||||
person.ProfileCropX = personBulkDto.Data.ProfileCropX;
|
||||
person.ProfileCropY = personBulkDto.Data.ProfileCropY;
|
||||
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom;
|
||||
person.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
personRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a person by ID.
|
||||
/// Deletes a person by ID (soft delete).
|
||||
/// </summary>
|
||||
/// <param name="id">The person ID.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
/// <returns>200 on success, 404 if not found.</returns>
|
||||
/// <summary>
|
||||
/// Deletes a person by ID (hard delete). Cascades to unlink all associated albums.
|
||||
/// </summary>
|
||||
/// <returns>200 on success, 404 if not found.</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IStatusCodeActionResult Delete([FromRoute] Guid id)=>throw new NotImplementedException();
|
||||
|
||||
public IActionResult Delete([FromRoute] Guid id) {
|
||||
var person = personRepository.Find(id);
|
||||
if (person == null) return NotFound();
|
||||
|
||||
// Unlink all albums owned by this person
|
||||
var albums = albumRepository.FindByPerson(id).ToList();
|
||||
foreach (var album in albums) {
|
||||
album.PersonOwnerId = null;
|
||||
}
|
||||
|
||||
personRepository.Remove(person);
|
||||
personRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes multiple people in a bulk operation.
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of person IDs to delete.</param>
|
||||
/// <returns>200 on success.</returns>
|
||||
/// <summary>
|
||||
/// Deletes multiple people in a bulk operation (hard delete). Cascades to unlink all associated albums.
|
||||
/// </summary>
|
||||
/// <returns>200 on success.</returns>
|
||||
[HttpDelete]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IStatusCodeActionResult BulkDelete([FromBody] List<Guid> ids)=>throw new NotImplementedException();
|
||||
|
||||
public IActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||
var people = personRepository.FindBulk(ids).ToList();
|
||||
|
||||
foreach (var person in people) {
|
||||
// Unlink all albums owned by this person
|
||||
var albums = albumRepository.FindByPerson(person.Id).ToList();
|
||||
foreach (var album in albums) {
|
||||
album.PersonOwnerId = null;
|
||||
}
|
||||
personRepository.Remove(person);
|
||||
}
|
||||
|
||||
personRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new person.
|
||||
/// </summary>
|
||||
@@ -73,5 +187,15 @@ public class PersonController : ControllerBase {
|
||||
/// <returns>200 with the new person ID.</returns>
|
||||
[HttpPut]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public ActionResult<Guid> Create([FromBody] PersonCreateDto dto)=>throw new NotImplementedException();
|
||||
public ActionResult<Guid> Create([FromBody] PersonCreateDto dto) {
|
||||
var person = new Models.Person {
|
||||
Id = Guid.NewGuid(),
|
||||
Name = dto.Name,
|
||||
ProfileAssetId = dto.ProfileAssetId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
personRepository.Insert(person);
|
||||
personRepository.Save();
|
||||
return Ok(person.Id);
|
||||
}
|
||||
}
|
||||
30
Lactose/Controllers/StatsController.cs
Normal file
30
Lactose/Controllers/StatsController.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Butter.Dtos.Stats;
|
||||
using Lactose.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Lactose.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides aggregate statistics about the MilkyShots instance.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="statsRepository">Stats repository.</param>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
public class StatsController(
|
||||
ILogger<StatsController> logger,
|
||||
IStatsRepository statsRepository
|
||||
) : ControllerBase {
|
||||
/// <summary>
|
||||
/// Returns comprehensive aggregate statistics about the instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="StatsDto"/> with all gathered statistics.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<StatsDto> Get() {
|
||||
logger.LogTrace("Stats requested");
|
||||
var stats = statsRepository.GetStats();
|
||||
return Ok(stats);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,10 @@ public class TagController(
|
||||
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
||||
/// <returns>A list of tags matching the search criteria.</returns>
|
||||
[HttpGet]
|
||||
public ActionResult<List<TagDto>> GetAll(PagedSearchParametersDto pagedSearch) {
|
||||
public ActionResult<List<TagDto>> GetAll([FromQuery] PagedSearchParametersDto pagedSearch) {
|
||||
pagedSearch.Search ??= string.Empty;
|
||||
if(pagedSearch.Page < 0) { return BadRequest("Page must be greater than 0"); }
|
||||
if(pagedSearch.PageSize < 0) { return BadRequest("PageSize must be greater than 0"); }
|
||||
if(pagedSearch.PageSize < 1 || pagedSearch.PageSize > PagedParametersDto.MaxPageSize) { return BadRequest($"PageSize must be between 1 and {PagedParametersDto.MaxPageSize}"); }
|
||||
|
||||
List<Tag> tags = tagRepository.Search(pagedSearch.Search, pagedSearch.Page, pagedSearch.PageSize);
|
||||
List<TagDto> tagsDto = [];
|
||||
@@ -92,7 +92,7 @@ public class TagController(
|
||||
/// <param name="id">The tag ID.</param>
|
||||
/// <param name="tagDto">The updated tag data.</param>
|
||||
/// <returns>200 on success, 404 if not found, 409 if name conflict.</returns>
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPost("{id}")]
|
||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto) {
|
||||
|
||||
@@ -108,7 +108,7 @@ public class TagController(
|
||||
/// </summary>
|
||||
/// <param name="tagBulkDto">The bulk update data with IDs and shared update values.</param>
|
||||
/// <returns>200 on success, or the first error response.</returns>
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPost]
|
||||
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<TagUpdateDto> tagBulkDto) {
|
||||
|
||||
@@ -127,7 +127,7 @@ public class TagController(
|
||||
/// </summary>
|
||||
/// <param name="id">The tag ID.</param>
|
||||
/// <returns>200 on success, 404 if not found.</returns>
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpDelete("{id}")]
|
||||
public ActionResult Delete([FromRoute] Guid id) {
|
||||
IStatusCodeActionResult result = DeleteTag(id);
|
||||
@@ -142,7 +142,7 @@ public class TagController(
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of tag IDs to delete.</param>
|
||||
/// <returns>200 on success, or the first error response.</returns>
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpDelete]
|
||||
public IActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||
foreach (Guid id in ids) {
|
||||
@@ -160,7 +160,7 @@ public class TagController(
|
||||
/// </summary>
|
||||
/// <param name="tagDto">The tag creation data.</param>
|
||||
/// <returns>200 on success, 409 if a tag with the same name already exists, 400 if parent not found.</returns>
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||
[HttpPut]
|
||||
public ActionResult Create([FromBody] TagCreateDto tagDto) {
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ namespace Lactose.Controllers {
|
||||
"""
|
||||
);
|
||||
|
||||
if (userRepository.GetAll().Any(x => x.Email == userCreateDto.Email)) return Conflict("Email already exists");
|
||||
if (userRepository.GetAll().Any(x => x.Username == userCreateDto.Username)) return Conflict("Username already exists");
|
||||
|
||||
var user = new User {
|
||||
Username = userCreateDto.Username,
|
||||
Email = userCreateDto.Email,
|
||||
@@ -57,8 +60,7 @@ namespace Lactose.Controllers {
|
||||
AccessLevel = userCreateDto.AccessLevel ?? EAccessLevel.User
|
||||
};
|
||||
|
||||
if (userRepository.GetAll().Any(x => x.Email == user.Email)) return Conflict("Email already exists");
|
||||
if (userRepository.GetAll().Any(x => x.Username == user.Username)) return Conflict("Username already exists");
|
||||
user.Password = passwordHasher.HashPassword(user, userCreateDto.Password);
|
||||
|
||||
userRepository.Insert(user);
|
||||
userRepository.Save();
|
||||
@@ -81,34 +83,28 @@ namespace Lactose.Controllers {
|
||||
return Unauthorized("Unauthorized");
|
||||
}
|
||||
|
||||
//TODO: Lower level of access should fill less information of users
|
||||
logger.LogDebug($"Sending user with id {id}");
|
||||
|
||||
User? user = userRepository.Find(id);
|
||||
User? user = userRepository.FindWithMaintainedPersons(id);
|
||||
|
||||
if (user is null) return NotFound();
|
||||
|
||||
return Ok(user.ToGetUsersDto());
|
||||
return Ok(user.ToGetUsersDto(authenticatedUser.AccessLevel, authenticatedUser.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all users.
|
||||
/// Gets all users (admin/curator only).
|
||||
/// </summary>
|
||||
/// <returns>A list of all users.</returns>
|
||||
[Authorize(Roles = "Admin,Curator")]
|
||||
[HttpGet]
|
||||
public ActionResult<List<UserInfoDto>> GetAll() {
|
||||
logger.LogDebug("Sending Users list");
|
||||
LactoseAuthenticatedUser? authenticatedUser = authService.GetUserData(User);
|
||||
EAccessLevel viewerAccessLevel = authenticatedUser?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
if (authenticatedUser == null) {
|
||||
logger.LogWarning(@"Anonymous User hasn't enough permissions to create a User. it shouldn't be here");
|
||||
return Unauthorized("Unauthorized");
|
||||
}
|
||||
logger.LogDebug("Sending Users list");
|
||||
|
||||
//TODO: Lower level of access should fill less information of users
|
||||
//if (false) return Unauthorized();
|
||||
|
||||
return Ok(userRepository.GetAll().Select(u => u.ToGetUsersDto()).ToList());
|
||||
return Ok(userRepository.GetAll().Select(u => u.ToGetUsersDto(viewerAccessLevel, authenticatedUser?.Id)).ToList());
|
||||
}
|
||||
|
||||
|
||||
@@ -147,20 +143,41 @@ namespace Lactose.Controllers {
|
||||
|
||||
if (dto.Email != null) { user.Email = dto.Email; }
|
||||
|
||||
if (dto.Password != null) { user.Password = passwordHasher.HashPassword(user, dto.Password); }
|
||||
if (dto.Password != null) {
|
||||
if (dto.OldPassword != null) {
|
||||
if (passwordHasher.VerifyHashedPassword(user, user.Password, dto.OldPassword) == PasswordVerificationResult.Failed) {
|
||||
return BadRequest("Old password is incorrect");
|
||||
}
|
||||
} else if (authenticatedUser.AccessLevel != EAccessLevel.Admin) {
|
||||
return BadRequest("Old password is required to change your own password");
|
||||
}
|
||||
|
||||
if (dto.IsDeleted != null) { user.DeletedAt = dto.IsDeleted.Value ? DateTime.Now : null; }
|
||||
if (authenticatedUser.AccessLevel != EAccessLevel.Admin) {
|
||||
if (dto.Password.Length < 8) return BadRequest("Password must be at least 8 characters long");
|
||||
if (!dto.Password.Any(char.IsUpper)) return BadRequest("Password must contain at least one uppercase letter");
|
||||
if (!dto.Password.Any(char.IsLower)) return BadRequest("Password must contain at least one lowercase letter");
|
||||
if (!dto.Password.Any(c => char.IsDigit(c) || char.IsPunctuation(c) || char.IsSymbol(c))) return BadRequest("Password must contain at least one number or special character");
|
||||
}
|
||||
|
||||
user.Password = passwordHasher.HashPassword(user, dto.Password);
|
||||
}
|
||||
|
||||
if (dto.IsDeleted != null) { user.DeletedAt = dto.IsDeleted.Value ? DateTime.UtcNow : null; }
|
||||
|
||||
if (authenticatedUser.AccessLevel == EAccessLevel.Admin) {
|
||||
if (dto.IsBanned != null) {
|
||||
if (user.BannedAt != null) user.BannedAt = dto.IsBanned.Value ? DateTime.Now : null;
|
||||
if (user.BannedAt != null) user.BannedAt = dto.IsBanned.Value ? DateTime.UtcNow : null;
|
||||
}
|
||||
|
||||
if (dto.AccessLevel != null) { user.AccessLevel = dto.AccessLevel.Value; }
|
||||
|
||||
if (dto.MaintainedPersonIds != null) {
|
||||
userRepository.SetMaintainedPersons(user.Id, dto.MaintainedPersonIds);
|
||||
}
|
||||
}
|
||||
|
||||
userRepository.Save();
|
||||
return Ok(user.ToGetUsersDto());
|
||||
return Ok(user.ToGetUsersDto(authenticatedUser.AccessLevel, authenticatedUser.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -100,5 +100,36 @@
|
||||
"365",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Thumbnail Quality",
|
||||
"Value": "75",
|
||||
"Description": "WebP encoding quality for thumbnails (1-100).",
|
||||
"Type": 1,
|
||||
"DisplayType": 7,
|
||||
"Options": [
|
||||
"1",
|
||||
"100",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Preview Quality",
|
||||
"Value": "85",
|
||||
"Description": "WebP encoding quality for previews (1-100).",
|
||||
"Type": 1,
|
||||
"DisplayType": 7,
|
||||
"Options": [
|
||||
"1",
|
||||
"100",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Job Batch Size",
|
||||
"Value": "200",
|
||||
"Description": "Number of assets to process per sub-job batch.",
|
||||
"Type": 1,
|
||||
"DisplayType": 2
|
||||
}
|
||||
]
|
||||
@@ -1,11 +1,12 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
RUN mkdir -p /app/thumbnails /app/previews && chown -R $APP_UID /app
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
EXPOSE 5162
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR "/src"
|
||||
COPY ["Lactose/Lactose.csproj", "Lactose/"]
|
||||
@@ -15,11 +16,10 @@ WORKDIR "/src/Lactose"
|
||||
RUN dotnet build "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir -p thumbnails previews
|
||||
|
||||
ENTRYPOINT ["dotnet", "Lactose.dll"]
|
||||
|
||||
410
Lactose/Jobs/CreateAlbumsJob.cs
Normal file
410
Lactose/Jobs/CreateAlbumsJob.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using Butter.Types;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Scans active folders with regex patterns in a streaming fashion (suitable for millions of assets),
|
||||
/// creates missing Person and Album entities, and assigns matching assets to their albums.
|
||||
/// </summary>
|
||||
public class CreateAlbumsJob(ILogger<CreateAlbumsJob> logger, IServiceProvider serviceProvider) : Job {
|
||||
const int ScanBatchSize = 5000;
|
||||
const int SaveBatchSize = 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Create Albums Job";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task TaskJob(CancellationToken token) {
|
||||
try {
|
||||
logger.LogInformation("Starting create albums job...");
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
|
||||
var folderRepo = scope.ServiceProvider.GetRequiredService<IFolderRepository>();
|
||||
var personRepo = scope.ServiceProvider.GetRequiredService<IPersonRepository>();
|
||||
var albumRepo = scope.ServiceProvider.GetRequiredService<IAlbumRepository>();
|
||||
|
||||
var folders = folderRepo.GetAll()
|
||||
.Where(f => f.Active && !string.IsNullOrWhiteSpace(f.RegexPattern))
|
||||
.ToList();
|
||||
|
||||
if (folders.Count == 0) {
|
||||
logger.LogInformation("No active folders with a regex pattern found.");
|
||||
JobStatus.Complete("No active folders with a regex pattern found.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Scanning {FolderCount} active folders for person/album patterns...", folders.Count);
|
||||
|
||||
// ── Phase 1: Streaming scan → collect unique (person, album) names only ──
|
||||
var personAlbumNames = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
float folderIndex = 0;
|
||||
int uniquePersons = 0;
|
||||
int uniqueAlbums = 0;
|
||||
int totalAssetsScanned = 0;
|
||||
int totalAssetsMatched = 0;
|
||||
|
||||
foreach (var folder in folders) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var regex = new Regex(folder.RegexPattern!, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
var groupNames = regex.GetGroupNames().Select(g => g.ToLowerInvariant()).ToHashSet();
|
||||
|
||||
if (!groupNames.Contains("person") || !groupNames.Contains("group")) {
|
||||
logger.LogWarning("Folder {FolderId} ({BasePath}) regex lacks 'person' and/or 'group' named groups. Skipping.", folder.Id, folder.BasePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var totalInFolder = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.CountAsync(token);
|
||||
|
||||
int folderScanned = 0;
|
||||
int folderMatched = 0;
|
||||
|
||||
for (int offset = 0; offset < totalInFolder; offset += ScanBatchSize) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var paths = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(ScanBatchSize)
|
||||
.Select(a => a.OriginalPath)
|
||||
.ToListAsync(token);
|
||||
|
||||
int batchMatched = 0;
|
||||
foreach (var path in paths) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var match = regex.Match(path);
|
||||
if (!match.Success) continue;
|
||||
|
||||
var personGroup = match.Groups["person"];
|
||||
var groupGroup = match.Groups["group"];
|
||||
if (!personGroup.Success || !groupGroup.Success) continue;
|
||||
|
||||
var personName = ToTitleCase(personGroup.Value.Replace('_', ' ').Trim());
|
||||
var albumName = ToTitleCase(groupGroup.Value.Replace('_', ' ').Trim());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(personName) || string.IsNullOrWhiteSpace(albumName))
|
||||
continue;
|
||||
|
||||
if (!personAlbumNames.TryGetValue(personName, out var albums)) {
|
||||
albums = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
personAlbumNames[personName] = albums;
|
||||
uniquePersons++;
|
||||
}
|
||||
if (albums.Add(albumName))
|
||||
uniqueAlbums++;
|
||||
|
||||
batchMatched++;
|
||||
}
|
||||
|
||||
folderScanned += paths.Count;
|
||||
folderMatched += batchMatched;
|
||||
totalAssetsScanned += paths.Count;
|
||||
totalAssetsMatched += batchMatched;
|
||||
|
||||
// Keep progress bar at per-folder level, but update the message per batch
|
||||
// so the expanded details panel shows live counts
|
||||
float progress = folderIndex / folders.Count * 0.15f;
|
||||
JobStatus.UpdateProgress(progress,
|
||||
$"Scanning {folder.BasePath}: {folderScanned:N0}/{totalInFolder:N0} — {uniquePersons} persons, {uniqueAlbums} albums found so far");
|
||||
}
|
||||
|
||||
folderIndex++;
|
||||
float folderProgress = folderIndex / folders.Count * 0.15f;
|
||||
JobStatus.UpdateProgress(folderProgress,
|
||||
$"Scanned folder {folder.BasePath}: {folderScanned:N0} assets, {folderMatched} matched — {uniquePersons} persons, {uniqueAlbums} albums");
|
||||
|
||||
logger.LogInformation("Scanned folder {BasePath}: {Scanned:N0} assets, {Matched} matched. Cumulative: {Persons} persons, {Albums} albums",
|
||||
folder.BasePath, folderScanned, folderMatched, uniquePersons, uniqueAlbums);
|
||||
}
|
||||
|
||||
if (personAlbumNames.Count == 0) {
|
||||
logger.LogInformation("No person/album pairs extracted from asset paths.");
|
||||
JobStatus.Complete("No person/album pairs extracted from asset paths.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalGroups = personAlbumNames.Sum(kv => kv.Value.Count);
|
||||
logger.LogInformation("Discovery complete: {PersonCount} persons, {GroupCount} album groups from {Matched:N0} matched assets.",
|
||||
personAlbumNames.Count, totalGroups, totalAssetsMatched);
|
||||
JobStatus.UpdateProgress(0.15f, $"Discovered {personAlbumNames.Count} persons, {totalGroups} albums from {totalAssetsMatched:N0} matched assets.");
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// ── Phase 2: Create missing persons and albums ──
|
||||
|
||||
var personNames = personAlbumNames.Keys;
|
||||
var existingPersons = personRepo.FindByNames(personNames)
|
||||
.ToDictionary(p => p.Name, p => p.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var missingPersonCount = personNames.Count(n => !existingPersons.ContainsKey(n));
|
||||
if (missingPersonCount > 0)
|
||||
logger.LogInformation("{Existing} existing persons found, {Missing} missing to create.", existingPersons.Count, missingPersonCount);
|
||||
else
|
||||
logger.LogInformation("All {Count} persons already exist.", existingPersons.Count);
|
||||
|
||||
var personCreated = 0;
|
||||
foreach (var name in personNames) {
|
||||
if (existingPersons.ContainsKey(name)) continue;
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
var person = new Person {
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
personRepo.Insert(person);
|
||||
existingPersons[name] = person.Id;
|
||||
personCreated++;
|
||||
|
||||
if (personCreated % 10 == 0) {
|
||||
JobStatus.UpdateProgress(0.15f + (float)personCreated / missingPersonCount * 0.10f,
|
||||
$"Creating persons: {personCreated}/{missingPersonCount} — {name}");
|
||||
}
|
||||
}
|
||||
|
||||
if (personCreated > 0) {
|
||||
personRepo.Save();
|
||||
logger.LogInformation("Created {Count} missing persons.", personCreated);
|
||||
}
|
||||
|
||||
JobStatus.UpdateProgress(0.25f, $"Persons resolved (created {personCreated} new). Checking existing albums...");
|
||||
|
||||
var existingAlbumKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var allAlbumTitles = personAlbumNames.Values.SelectMany(v => v).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
if (allAlbumTitles.Count > 0) {
|
||||
var existing = await db.Albums
|
||||
.Where(a => allAlbumTitles.Contains(a.Title))
|
||||
.Select(a => new { a.Title, a.PersonOwnerId })
|
||||
.ToListAsync(token);
|
||||
foreach (var a in existing)
|
||||
existingAlbumKeys.Add($"{a.Title}|{a.PersonOwnerId}");
|
||||
}
|
||||
|
||||
logger.LogInformation("Found {Count} existing album-person pairings in DB.", existingAlbumKeys.Count);
|
||||
|
||||
var albumIdMap = new Dictionary<(string Person, string Album), Guid>();
|
||||
int albumCreated = 0;
|
||||
int totalAlbumSlots = personAlbumNames.Sum(kv => kv.Value.Count);
|
||||
int newAlbumSlots = totalAlbumSlots - existingAlbumKeys.Count;
|
||||
int albumsChecked = 0;
|
||||
|
||||
if (newAlbumSlots > 0)
|
||||
logger.LogInformation("{New} new albums to create out of {Total} total.", newAlbumSlots, totalAlbumSlots);
|
||||
|
||||
foreach (var (personName, albumNames) in personAlbumNames) {
|
||||
if (!existingPersons.TryGetValue(personName, out var personId)) continue;
|
||||
|
||||
foreach (var albumName in albumNames) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
albumsChecked++;
|
||||
var existsKey = $"{albumName}|{personId}";
|
||||
if (existingAlbumKeys.Contains(existsKey)) continue;
|
||||
|
||||
var albumId = Guid.NewGuid();
|
||||
albumRepo.Insert(new Album {
|
||||
Id = albumId,
|
||||
Title = albumName,
|
||||
PersonOwnerId = personId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
albumIdMap[(personName, albumName)] = albumId;
|
||||
existingAlbumKeys.Add(existsKey);
|
||||
albumCreated++;
|
||||
|
||||
if (albumCreated % 10 == 0) {
|
||||
JobStatus.UpdateProgress(0.25f + (float)albumsChecked / totalAlbumSlots * 0.15f,
|
||||
$"Creating albums: {albumCreated}/{newAlbumSlots} new — {albumName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (albumCreated > 0) {
|
||||
albumRepo.Save();
|
||||
logger.LogInformation("Created {Count} new albums.", albumCreated);
|
||||
}
|
||||
|
||||
// Build lookup for the assignment phase (covers both new and pre-existing albums)
|
||||
var albumLookup = new Dictionary<(string Person, string Album), Guid>();
|
||||
foreach (var (personName, albumNames) in personAlbumNames) {
|
||||
if (!existingPersons.TryGetValue(personName, out var personId)) continue;
|
||||
|
||||
foreach (var albumName in albumNames) {
|
||||
if (albumIdMap.TryGetValue((personName, albumName), out var id)) {
|
||||
albumLookup[(personName, albumName)] = id;
|
||||
} else {
|
||||
var existing = await db.Albums
|
||||
.Where(a => a.Title == albumName && a.PersonOwnerId == personId)
|
||||
.Select(a => a.Id)
|
||||
.FirstOrDefaultAsync(token);
|
||||
if (existing != Guid.Empty)
|
||||
albumLookup[(personName, albumName)] = existing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JobStatus.UpdateProgress(0.40f, $"Entities complete: {personCreated} persons, {albumCreated} albums created. Assigning assets...");
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// ── Phase 3: Re-scan assets in batches, assign to albums via Asset.Albums ──
|
||||
int assetAssignments = 0;
|
||||
int pendingBeforeSave = 0;
|
||||
folderIndex = 0;
|
||||
|
||||
foreach (var folder in folders) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var regex = new Regex(folder.RegexPattern!, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
var groupNames = regex.GetGroupNames().Select(g => g.ToLowerInvariant()).ToHashSet();
|
||||
|
||||
if (!groupNames.Contains("person") || !groupNames.Contains("group"))
|
||||
continue;
|
||||
|
||||
var totalInFolder = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.CountAsync(token);
|
||||
|
||||
int folderAssigned = 0;
|
||||
int folderScanned = 0;
|
||||
|
||||
for (int offset = 0; offset < totalInFolder; offset += ScanBatchSize) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var assets = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(ScanBatchSize)
|
||||
.Select(a => new { a.Id, a.OriginalPath })
|
||||
.ToListAsync(token);
|
||||
|
||||
var batchAssetIds = assets.Select(a => a.Id).ToList();
|
||||
var existingPairs = new HashSet<(Guid AlbumId, Guid AssetId)>();
|
||||
if (batchAssetIds.Count > 0) {
|
||||
var existing = await db.Albums
|
||||
.Where(a => a.Assets!.Any(asset => batchAssetIds.Contains(asset.Id)))
|
||||
.SelectMany(a => a.Assets!.Select(asset => new { AlbumId = a.Id, AssetId = asset.Id }))
|
||||
.Where(x => batchAssetIds.Contains(x.AssetId))
|
||||
.Select(x => new { x.AlbumId, x.AssetId })
|
||||
.ToListAsync(token);
|
||||
foreach (var pair in existing)
|
||||
existingPairs.Add((pair.AlbumId, pair.AssetId));
|
||||
}
|
||||
|
||||
foreach (var asset in assets) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var match = regex.Match(asset.OriginalPath);
|
||||
if (!match.Success) continue;
|
||||
|
||||
var personGroup = match.Groups["person"];
|
||||
var groupGroup = match.Groups["group"];
|
||||
if (!personGroup.Success || !groupGroup.Success) continue;
|
||||
|
||||
var personName = ToTitleCase(personGroup.Value.Replace('_', ' ').Trim());
|
||||
var albumName = ToTitleCase(groupGroup.Value.Replace('_', ' ').Trim());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(personName) || string.IsNullOrWhiteSpace(albumName))
|
||||
continue;
|
||||
|
||||
if (!albumLookup.TryGetValue((personName, albumName), out var albumId))
|
||||
continue;
|
||||
|
||||
var assetStub = db.Assets.Local.FirstOrDefault(a => a.Id == asset.Id);
|
||||
if (assetStub == null) {
|
||||
assetStub = new Asset {
|
||||
Id = asset.Id,
|
||||
OriginalPath = string.Empty,
|
||||
OriginalFilename = string.Empty,
|
||||
Type = EAssetType.Image,
|
||||
CreatedAt = DateTime.MinValue
|
||||
};
|
||||
db.Entry(assetStub).State = EntityState.Unchanged;
|
||||
}
|
||||
|
||||
var albumStub = db.Albums.Local.FirstOrDefault(a => a.Id == albumId);
|
||||
if (albumStub == null) {
|
||||
albumStub = new Album { Id = albumId };
|
||||
db.Entry(albumStub).State = EntityState.Unchanged;
|
||||
}
|
||||
|
||||
if (assetStub.Albums == null)
|
||||
assetStub.Albums = new List<Album>();
|
||||
|
||||
if (!existingPairs.Contains((albumId, asset.Id)) && !assetStub.Albums.Any(a => a.Id == albumId)) {
|
||||
assetStub.Albums.Add(albumStub);
|
||||
assetAssignments++;
|
||||
folderAssigned++;
|
||||
}
|
||||
pendingBeforeSave++;
|
||||
|
||||
if (pendingBeforeSave >= SaveBatchSize) {
|
||||
await db.SaveChangesAsync(token);
|
||||
db.ChangeTracker.Clear();
|
||||
pendingBeforeSave = 0;
|
||||
}
|
||||
}
|
||||
|
||||
folderScanned += assets.Count;
|
||||
|
||||
// Per-batch message update while keeping progress at per-folder level
|
||||
float batchScanFraction = Math.Min(1f, (float)(offset + ScanBatchSize) / totalInFolder);
|
||||
float progress = 0.40f + (folderIndex + batchScanFraction) / folders.Count * 0.60f;
|
||||
JobStatus.UpdateProgress(progress,
|
||||
$"Assigning in {folder.BasePath}: {Math.Min(offset + ScanBatchSize, totalInFolder):N0}/{totalInFolder:N0} — {assetAssignments:N0} total");
|
||||
}
|
||||
|
||||
folderIndex++;
|
||||
float folderProgress = 0.40f + folderIndex / folders.Count * 0.60f;
|
||||
JobStatus.UpdateProgress(folderProgress,
|
||||
$"Assigned {folderAssigned:N0} assets in {folder.BasePath} ({assetAssignments:N0} total so far)");
|
||||
|
||||
logger.LogInformation("Assigned {FolderAssignments:N0} assets in folder {BasePath} ({Total:N0} total)",
|
||||
folderAssigned, folder.BasePath, assetAssignments);
|
||||
}
|
||||
|
||||
if (pendingBeforeSave > 0)
|
||||
await db.SaveChangesAsync(token);
|
||||
|
||||
db.ChangeTracker.Clear();
|
||||
|
||||
logger.LogInformation("Setting cover images for albums without one...");
|
||||
int coversAssigned = 0;
|
||||
var albumsNeedingCover = await db.Albums
|
||||
.Where(a => a.CoverAssetId == null)
|
||||
.ToListAsync(token);
|
||||
|
||||
foreach (var album in albumsNeedingCover) {
|
||||
var firstAsset = await db.Assets
|
||||
.Where(a => a.Albums!.Any(al => al.Id == album.Id) && a.DeletedAt == null)
|
||||
.OrderBy(a => a.OriginalFilename)
|
||||
.Select(a => new { a.Id })
|
||||
.FirstOrDefaultAsync(token);
|
||||
|
||||
if (firstAsset != null) {
|
||||
album.CoverAssetId = firstAsset.Id;
|
||||
coversAssigned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (coversAssigned > 0) {
|
||||
await db.SaveChangesAsync(token);
|
||||
logger.LogInformation("Assigned cover images to {Count} albums.", coversAssigned);
|
||||
}
|
||||
|
||||
logger.LogInformation("Completed: {Assigned} assets assigned across {PersonsCreated} persons, {AlbumsCreated} albums.",
|
||||
assetAssignments, personCreated, albumCreated);
|
||||
JobStatus.Complete(
|
||||
$"Created {personCreated} persons, {albumCreated} albums, assigned {assetAssignments:N0} assets.");
|
||||
} catch (OperationCanceledException) {
|
||||
JobStatus.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
static string ToTitleCase(string input) =>
|
||||
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLowerInvariant());
|
||||
}
|
||||
160
Lactose/Jobs/CreatePersonsJob.cs
Normal file
160
Lactose/Jobs/CreatePersonsJob.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Scans active folders with regex patterns in a streaming fashion (suitable for millions of assets),
|
||||
/// extracts person names from asset paths, and creates any missing Person entities.
|
||||
/// </summary>
|
||||
public class CreatePersonsJob(ILogger<CreatePersonsJob> logger, IServiceProvider serviceProvider) : Job {
|
||||
const int ScanBatchSize = 5000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Create Persons Job";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task TaskJob(CancellationToken token) {
|
||||
try {
|
||||
logger.LogInformation("Starting create persons job...");
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
|
||||
var folderRepo = scope.ServiceProvider.GetRequiredService<IFolderRepository>();
|
||||
var personRepo = scope.ServiceProvider.GetRequiredService<IPersonRepository>();
|
||||
|
||||
var folders = folderRepo.GetAll()
|
||||
.Where(f => f.Active && !string.IsNullOrWhiteSpace(f.RegexPattern))
|
||||
.ToList();
|
||||
|
||||
if (folders.Count == 0) {
|
||||
logger.LogInformation("No active folders with a regex pattern found.");
|
||||
JobStatus.Complete("No active folders with a regex pattern found.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Scanning {FolderCount} active folders for person names...", folders.Count);
|
||||
|
||||
// ── Phase 1: Streaming scan → collect unique person names ──
|
||||
var personNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
float folderIndex = 0;
|
||||
int totalAssetsMatched = 0;
|
||||
|
||||
foreach (var folder in folders) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var regex = new Regex(folder.RegexPattern!, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
if (!regex.GetGroupNames().Contains("person", StringComparer.OrdinalIgnoreCase)) {
|
||||
logger.LogWarning("Folder {FolderId} ({BasePath}) regex is missing the 'person' named group. Skipping.", folder.Id, folder.BasePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var totalInFolder = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.CountAsync(token);
|
||||
|
||||
int folderScanned = 0;
|
||||
int folderMatched = 0;
|
||||
|
||||
for (int offset = 0; offset < totalInFolder; offset += ScanBatchSize) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var paths = await db.Assets
|
||||
.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.OrderBy(a => a.Id)
|
||||
.Skip(offset)
|
||||
.Take(ScanBatchSize)
|
||||
.Select(a => a.OriginalPath)
|
||||
.ToListAsync(token);
|
||||
|
||||
int batchMatched = 0;
|
||||
foreach (var path in paths) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
var match = regex.Match(path);
|
||||
if (!match.Success) continue;
|
||||
|
||||
var group = match.Groups["person"];
|
||||
if (!group.Success) continue;
|
||||
|
||||
var name = ToTitleCase(group.Value.Replace('_', ' ').Trim());
|
||||
if (!string.IsNullOrWhiteSpace(name)) {
|
||||
personNames.Add(name);
|
||||
batchMatched++;
|
||||
}
|
||||
}
|
||||
|
||||
folderScanned += paths.Count;
|
||||
folderMatched += batchMatched;
|
||||
|
||||
// Per-batch message update while keeping progress at per-folder level
|
||||
float progress = folderIndex / folders.Count * 0.5f;
|
||||
JobStatus.UpdateProgress(progress,
|
||||
$"Scanning {folder.BasePath}: {folderScanned:N0}/{totalInFolder:N0} — {personNames.Count:N0} persons found so far");
|
||||
}
|
||||
|
||||
totalAssetsMatched += folderMatched;
|
||||
folderIndex++;
|
||||
float folderProgress = folderIndex / folders.Count * 0.5f;
|
||||
JobStatus.UpdateProgress(folderProgress,
|
||||
$"Scanned folder {folder.BasePath}: {folderScanned:N0} assets, {folderMatched:N0} matched — {personNames.Count:N0} unique persons");
|
||||
|
||||
logger.LogInformation("Scanned folder {BasePath}: {Scanned:N0} assets, {Matched} matched. Cumulative: {Persons} unique persons",
|
||||
folder.BasePath, folderScanned, folderMatched, personNames.Count);
|
||||
}
|
||||
|
||||
if (personNames.Count == 0) {
|
||||
logger.LogInformation("No person names extracted from asset paths.");
|
||||
JobStatus.Complete("No person names extracted from asset paths.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Extracted {Count} unique person names from {Matched:N0} matched assets. Checking existing...",
|
||||
personNames.Count, totalAssetsMatched);
|
||||
JobStatus.UpdateProgress(0.5f, $"Extracted {personNames.Count:N0} unique person names from {totalAssetsMatched:N0} matched assets.");
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
// ── Phase 2: Create missing persons ──
|
||||
|
||||
var existingNames = personRepo.FindByNames(personNames)
|
||||
.Select(p => p.Name)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var missingNames = personNames
|
||||
.Where(n => !existingNames.Contains(n))
|
||||
.ToList();
|
||||
|
||||
if (missingNames.Count == 0) {
|
||||
logger.LogInformation("All {Count} persons already exist.", personNames.Count);
|
||||
JobStatus.Complete($"All {personNames.Count:N0} persons already exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Creating {Count} missing persons ({Existing} already exist).",
|
||||
missingNames.Count, existingNames.Count);
|
||||
|
||||
float created = 0;
|
||||
foreach (var name in missingNames) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
personRepo.Insert(new Person {
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
created++;
|
||||
float progress = 0.5f + created / missingNames.Count * 0.5f;
|
||||
JobStatus.UpdateProgress(progress, $"Creating persons: {created:N0}/{missingNames.Count:N0} — {name}");
|
||||
}
|
||||
|
||||
personRepo.Save();
|
||||
logger.LogInformation("Created {Count} new persons.", missingNames.Count);
|
||||
JobStatus.Complete($"Created {missingNames.Count:N0} new persons from {totalAssetsMatched:N0} matched assets.");
|
||||
} catch (OperationCanceledException) {
|
||||
JobStatus.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
static string ToTitleCase(string input) =>
|
||||
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLowerInvariant());
|
||||
}
|
||||
@@ -75,7 +75,6 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
|
||||
foreach (var folder in folders) {
|
||||
if (token.IsCancellationRequested) {
|
||||
lock (childJobs) { childJobs.ForEach(j => j.Cancel()); }
|
||||
JobStatus.Cancel();
|
||||
return;
|
||||
}
|
||||
@@ -83,6 +82,7 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
steps++;
|
||||
var job = jobManager.CreateJob<FileSystemCrawlJob>(folderId, folder.FullName);
|
||||
job.ParentJobId = Id;
|
||||
job.LinkParentToken(token);
|
||||
job.Done += (o, _) => {
|
||||
lock (childJobs) { childJobs.Remove((o as Job)!); }
|
||||
if (Interlocked.Decrement(ref pendingChildren) == 0)
|
||||
@@ -97,7 +97,6 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
|
||||
foreach (var file in files) {
|
||||
if (token.IsCancellationRequested) {
|
||||
lock (childJobs) { childJobs.ForEach(j => j.Cancel()); }
|
||||
JobStatus.Cancel();
|
||||
return;
|
||||
}
|
||||
@@ -113,9 +112,9 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
assetRepository.Update(existing);
|
||||
assetRepository.Save();
|
||||
logger.LogInformation("Soft-deleted asset {Asset} — file no longer exists.", existing.OriginalPath);
|
||||
} else if (file.LastWriteTime > (existing.UpdatedAt ?? existing.CreatedAt)) {
|
||||
} else if (file.LastWriteTimeUtc > (existing.UpdatedAt ?? existing.CreatedAt)) {
|
||||
existing.FileSize = file.Length;
|
||||
existing.UpdatedAt = file.LastWriteTime;
|
||||
existing.UpdatedAt = file.LastWriteTimeUtc;
|
||||
assetRepository.Update(existing);
|
||||
assetRepository.Save();
|
||||
logger.LogInformation("Updated asset {Asset} — file modification detected.", existing.OriginalPath);
|
||||
@@ -138,7 +137,7 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
}
|
||||
|
||||
var childCount = Interlocked.CompareExchange(ref pendingChildren, 0, 0);
|
||||
JobStatus.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Waiting for {childCount} child jobs to complete.");
|
||||
JobStatus.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Waiting for {childCount:N0} child jobs to complete.");
|
||||
JobStatus.Wait();
|
||||
|
||||
if (childCount > 0) {
|
||||
@@ -146,7 +145,6 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
try {
|
||||
await childTcs.Task.ConfigureAwait(false);
|
||||
} catch (TaskCanceledException) {
|
||||
lock (childJobs) { childJobs.ForEach(j => j.Cancel()); }
|
||||
JobStatus.Cancel();
|
||||
return;
|
||||
}
|
||||
@@ -178,9 +176,9 @@ public sealed class FileSystemCrawlJob : Job {
|
||||
OriginalPath = finfo.FullName,
|
||||
OriginalFilename = finfo.Name,
|
||||
Type = type.Value,
|
||||
IsPubliclyShared = false,
|
||||
CreatedAt = finfo.CreationTime,
|
||||
UpdatedAt = finfo.LastWriteTime,
|
||||
Visibility = EVisibility.Private,
|
||||
CreatedAt = finfo.CreationTimeUtc,
|
||||
UpdatedAt = finfo.LastWriteTimeUtc,
|
||||
MimeType = type.Value switch {
|
||||
EAssetType.Image => mimeImg.MimeType,
|
||||
EAssetType.Video => mimeVid.MimeType,
|
||||
|
||||
@@ -9,192 +9,194 @@ namespace Lactose.Jobs;
|
||||
/// Performs a comprehensive integrity check of the system — database, assets, albums, people, thumbnails, and previews.
|
||||
/// </summary>
|
||||
public class IntegrityCheckJob (ILogger<IntegrityCheckJob> logger, IServiceProvider serviceProvider): Job{
|
||||
const float totalSteps = 8;
|
||||
|
||||
const float totalSteps = 7;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Integrity Check Job";
|
||||
|
||||
|
||||
void IncrementProgress(float f) => JobStatus.UpdateProgress(f/totalSteps, "Database connection successful.");
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task TaskJob(CancellationToken token) {
|
||||
try {
|
||||
|
||||
float progress = 0;
|
||||
logger.LogInformation("Starting integrity check job...");
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
|
||||
|
||||
//Step 1
|
||||
// Check database connection
|
||||
if (!dbContext.Database.CanConnect()) {
|
||||
logger.LogError("Cannot connect to the database.");
|
||||
JobStatus.Fail("Cannot connect to the database.");
|
||||
return;
|
||||
}
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 2
|
||||
// Check for pending migrations
|
||||
var pendingMigrations = dbContext.Database.GetPendingMigrations().ToList();
|
||||
float progress = 0;
|
||||
logger.LogInformation("Starting integrity check job...");
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
|
||||
|
||||
if (pendingMigrations.Any()) {
|
||||
logger.LogWarning("There are pending migrations:");
|
||||
|
||||
foreach (var migration in pendingMigrations) logger.LogWarning($"- {migration}");
|
||||
|
||||
// Apply migrations
|
||||
try {
|
||||
dbContext.Database.Migrate();
|
||||
logger.LogInformation("Applied pending migrations successfully.");
|
||||
} catch (Exception ex) {
|
||||
logger.LogError(ex, "Error applying migrations.");
|
||||
JobStatus.Fail("Error applying migrations.");
|
||||
//Step 1
|
||||
// Check database connection
|
||||
if (!dbContext.Database.CanConnect()) {
|
||||
logger.LogError("Cannot connect to the database.");
|
||||
JobStatus.Fail("Cannot connect to the database.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// A bit of context: Load() will load all entities from the table into the DbContext.
|
||||
// Essentially it will ask the DB for all rows in the table and create entities for them in the DbContext.
|
||||
// This will basically warm up the cache of the DbContext so that when we later access all the rows in the table,
|
||||
// it won't lazy load them one by one, one call at the time.
|
||||
// This is important because lazy loading one by one would be a huge performance hit,
|
||||
// especially if there are many rows in the table.
|
||||
// While for folders it might not be a big deal, this is just for consistency with all the other steps,
|
||||
// especially the asset one.
|
||||
|
||||
// Step 3
|
||||
// Check for folders existence
|
||||
dbContext.Folders.Load();
|
||||
foreach (Folder folder in dbContext.Folders.ToList()) {
|
||||
if (!Directory.Exists(folder.BasePath)) {
|
||||
logger.LogWarning($"Folder {folder.Id} does not exist. Deactivating.");
|
||||
folder.Active = false;
|
||||
dbContext.Folders.Update(folder);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
var affectedAssets = dbContext.Assets.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.ToList();
|
||||
foreach (var asset in affectedAssets) {
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
dbContext.Assets.Update(asset);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Step 2
|
||||
// Check for pending migrations
|
||||
var pendingMigrations = dbContext.Database.GetPendingMigrations().ToList();
|
||||
|
||||
if (pendingMigrations.Any()) {
|
||||
logger.LogWarning("There are pending migrations:");
|
||||
|
||||
foreach (var migration in pendingMigrations) logger.LogWarning($"- {migration}");
|
||||
|
||||
// Apply migrations
|
||||
try {
|
||||
dbContext.Database.Migrate();
|
||||
logger.LogInformation("Applied pending migrations successfully.");
|
||||
} catch (Exception ex) {
|
||||
logger.LogError(ex, "Error applying migrations.");
|
||||
JobStatus.Fail("Error applying migrations.");
|
||||
return;
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
logger.LogWarning("Soft-deleted {Count} assets associated with missing folder {FolderId}.", affectedAssets.Count, folder.Id);
|
||||
}
|
||||
}
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 4
|
||||
// Check for assets existence
|
||||
dbContext.Assets.Load();
|
||||
foreach (Asset asset in dbContext.Assets.Where(a => a.DeletedAt == null)) {
|
||||
if (File.Exists(asset.OriginalPath)) continue;
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
dbContext.Assets.Update(asset);
|
||||
logger.LogWarning($"Asset {asset.Id} does not exist. Soft-deleting.");
|
||||
}
|
||||
IncrementProgress(++progress);
|
||||
|
||||
int changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Soft-deleted {Changes} non-existent assets.", changes);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 5
|
||||
// Check for empty albums
|
||||
dbContext.Albums.Load();
|
||||
dbContext.Albums.Include(a => a.Assets)
|
||||
.Where(a => a.Assets!.Count == 0)
|
||||
.ToList().ForEach(a => {
|
||||
logger.LogWarning($"Album {a.Id} is empty. Removing from database.");
|
||||
dbContext.Albums.Remove(a);
|
||||
});
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Removed {Changes} empty albums from the database.", changes);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 6
|
||||
// Check for Persons without faces
|
||||
dbContext.People.Include(p => p.Faces)
|
||||
.Where(p => p.Faces != null && p.Faces.Count == 0 && p.DeletedAt == null)
|
||||
.AsEnumerable()
|
||||
.ForEach(p => {
|
||||
logger.LogWarning($"Person {p.Id} has no faces. Soft-deleting.");
|
||||
p.DeletedAt = DateTime.UtcNow;
|
||||
dbContext.People.Update(p);
|
||||
});
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Removed {Changes} people without faces from the database.", changes);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 7
|
||||
// Check for orphaned thumbnails
|
||||
dbContext.Settings.Load();
|
||||
dbContext.Assets.Load();
|
||||
var thumbnailPath = dbContext.Settings.First(s => s.Name == Settings.ThumbnailPath.AsString()).Value;
|
||||
// A bit of context: Load() will load all entities from the table into the DbContext.
|
||||
// Essentially it will ask the DB for all rows in the table and create entities for them in the DbContext.
|
||||
// This will basically warm up the cache of the DbContext so that when we later access all the rows in the table,
|
||||
// it won't lazy load them one by one, one call at the time.
|
||||
// This is important because lazy loading one by one would be a huge performance hit,
|
||||
// especially if there are many rows in the table.
|
||||
// While for folders it might not be a big deal, this is just for consistency with all the other steps,
|
||||
// especially the asset one.
|
||||
|
||||
if (!Directory.Exists(thumbnailPath)) {
|
||||
logger.LogWarning("Thumbnail path does not exist. Creating...");
|
||||
// Step 3
|
||||
// Check for folders existence
|
||||
dbContext.Folders.Load();
|
||||
foreach (Folder folder in dbContext.Folders.ToList()) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!Directory.Exists(folder.BasePath)) {
|
||||
logger.LogWarning($"Folder {folder.Id} does not exist. Deactivating.");
|
||||
folder.Active = false;
|
||||
dbContext.Folders.Update(folder);
|
||||
|
||||
try {
|
||||
if (thumbnailPath != null)
|
||||
Directory.CreateDirectory(thumbnailPath);
|
||||
} catch (Exception e) {
|
||||
logger.LogError(e, "Could not create thumbnail directory.");
|
||||
var affectedAssets = dbContext.Assets.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
|
||||
.ToList();
|
||||
foreach (var asset in affectedAssets) {
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
dbContext.Assets.Update(asset);
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
logger.LogWarning("Soft-deleted {Count} assets associated with missing folder {FolderId}.", affectedAssets.Count, folder.Id);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogWarning("Marking all assets as missing thumbnails.");
|
||||
dbContext.Assets.ForEach(a => a.ThumbnailPath = "");
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||
} else {
|
||||
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.ThumbnailPath)).AsEnumerable()
|
||||
.Where(a => !File.Exists(a.ThumbnailPath)).AsEnumerable()
|
||||
.ForEach(a => {
|
||||
logger.LogWarning($"Asset {a.Id} has a missing thumbnail. Marking as missing.");
|
||||
a.ThumbnailPath = "";
|
||||
dbContext.Assets.Update(a);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Step 4
|
||||
// Check for assets existence
|
||||
dbContext.Assets.Load();
|
||||
foreach (Asset asset in dbContext.Assets.Where(a => a.DeletedAt == null)) {
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (File.Exists(asset.OriginalPath)) continue;
|
||||
asset.DeletedAt = DateTime.UtcNow;
|
||||
dbContext.Assets.Update(asset);
|
||||
logger.LogWarning($"Asset {asset.Id} does not exist. Soft-deleting.");
|
||||
}
|
||||
|
||||
int changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Soft-deleted {Changes} non-existent assets.", changes);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Step 5
|
||||
// Check for empty albums
|
||||
dbContext.Albums.Load();
|
||||
dbContext.Albums.Include(a => a.Assets)
|
||||
.Where(a => a.Assets!.Count == 0)
|
||||
.ToList().ForEach(a => {
|
||||
logger.LogWarning($"Album {a.Id} is empty. Removing from database.");
|
||||
dbContext.Albums.Remove(a);
|
||||
});
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||
}
|
||||
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Step 8
|
||||
// Check for orphaned previews
|
||||
logger.LogInformation("Removed {Changes} empty albums from the database.", changes);
|
||||
IncrementProgress(++progress);
|
||||
|
||||
var previewPath = dbContext.Settings.First(s => s.Name == Settings.PreviewPath.AsString()).Value;
|
||||
if (!Directory.Exists(previewPath)) {
|
||||
logger.LogWarning("Preview path does not exist. Creating...");
|
||||
|
||||
try {
|
||||
if (previewPath != null)
|
||||
Directory.CreateDirectory(previewPath);
|
||||
} catch (Exception e) {
|
||||
logger.LogError(e, "Could not create preview directory.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Step 6
|
||||
// Check for orphaned thumbnails
|
||||
dbContext.Settings.Load();
|
||||
dbContext.Assets.Load();
|
||||
var thumbnailPath = dbContext.Settings.First(s => s.Name == Settings.ThumbnailPath.AsString()).Value;
|
||||
|
||||
if (!Directory.Exists(thumbnailPath)) {
|
||||
logger.LogWarning("Thumbnail path does not exist. Creating...");
|
||||
|
||||
try {
|
||||
if (thumbnailPath != null)
|
||||
Directory.CreateDirectory(thumbnailPath);
|
||||
} catch (Exception e) {
|
||||
logger.LogError(e, "Could not create thumbnail directory.");
|
||||
}
|
||||
|
||||
logger.LogWarning("Marking all assets as missing thumbnails.");
|
||||
dbContext.Assets.ForEach(a => a.ThumbnailPath = "");
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||
} else {
|
||||
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.ThumbnailPath)).AsEnumerable()
|
||||
.Where(a => !File.Exists(a.ThumbnailPath)).AsEnumerable()
|
||||
.ForEach(a => {
|
||||
logger.LogWarning($"Asset {a.Id} has a missing thumbnail. Marking as missing.");
|
||||
a.ThumbnailPath = "";
|
||||
dbContext.Assets.Update(a);
|
||||
});
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||
}
|
||||
|
||||
IncrementProgress(++progress);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Step 7
|
||||
// Check for orphaned previews
|
||||
|
||||
var previewPath = dbContext.Settings.First(s => s.Name == Settings.PreviewPath.AsString()).Value;
|
||||
if (!Directory.Exists(previewPath)) {
|
||||
logger.LogWarning("Preview path does not exist. Creating...");
|
||||
|
||||
try {
|
||||
if (previewPath != null)
|
||||
Directory.CreateDirectory(previewPath);
|
||||
} catch (Exception e) {
|
||||
logger.LogError(e, "Could not create preview directory.");
|
||||
}
|
||||
|
||||
logger.LogWarning("Marking all assets as missing previews.");
|
||||
dbContext.Assets.ForEach(a => a.PreviewPath = "");
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
|
||||
} else {
|
||||
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.PreviewPath)).AsEnumerable()
|
||||
.Where(a => !File.Exists(a.PreviewPath)).AsEnumerable()
|
||||
.ForEach(a => {
|
||||
logger.LogWarning($"Asset {a.Id} has a missing preview. Marking as missing.");
|
||||
a.PreviewPath = "";
|
||||
dbContext.Assets.Update(a);
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogWarning("Marking all assets as missing previews.");
|
||||
dbContext.Assets.ForEach(a => a.PreviewPath = "");
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
|
||||
} else {
|
||||
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.PreviewPath)).AsEnumerable()
|
||||
.Where(a => !File.Exists(a.PreviewPath)).AsEnumerable()
|
||||
.ForEach(a => {
|
||||
logger.LogWarning($"Asset {a.Id} has a missing preview. Marking as missing.");
|
||||
a.PreviewPath = "";
|
||||
dbContext.Assets.Update(a);
|
||||
});
|
||||
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Complete
|
||||
logger.LogInformation("Integrity check completed successfully.");
|
||||
JobStatus.Complete("Integrity check completed successfully.");
|
||||
} catch (OperationCanceledException) {
|
||||
JobStatus.Cancel();
|
||||
}
|
||||
changes = dbContext.SaveChanges();
|
||||
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
|
||||
|
||||
IncrementProgress(++progress);
|
||||
|
||||
// Complete
|
||||
logger.LogInformation("Integrity check completed successfully.");
|
||||
JobStatus.Complete("Integrity check completed successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Butter.Types;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
|
||||
@@ -32,22 +31,38 @@ public abstract class Job {
|
||||
public IServiceScope? Scope { get; set; }
|
||||
Task? task;
|
||||
CancellationTokenSource? cts;
|
||||
CancellationToken? _parentToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new job instance.
|
||||
/// </summary>
|
||||
protected Job() => JobStatus = new JobStatus(this);
|
||||
|
||||
/// <summary>
|
||||
/// Links this job's cancellation token to a parent token. When the parent token is cancelled,
|
||||
/// this job's token is also cancelled automatically — even before <see cref="Start"/> is called.
|
||||
/// </summary>
|
||||
/// <param name="parentToken">The parent job's cancellation token.</param>
|
||||
public void LinkParentToken(CancellationToken parentToken) {
|
||||
_parentToken = parentToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the task and the cancellation token, then starts the task (triggering the Started event)
|
||||
/// Also sets up a continuation to handle completion, failure, or cancellation of the task.
|
||||
/// </summary>
|
||||
public virtual void Start() {
|
||||
JobStatus.Start();
|
||||
cts = new CancellationTokenSource();
|
||||
cts = _parentToken.HasValue
|
||||
? CancellationTokenSource.CreateLinkedTokenSource(_parentToken.Value)
|
||||
: new CancellationTokenSource();
|
||||
task = Task.Run(() => TaskJob(cts.Token));
|
||||
|
||||
task.ContinueWith(t => {
|
||||
if (t.IsCanceled && JobStatus.Status != EJobStatus.Canceled) {
|
||||
JobStatus.Cancel();
|
||||
}
|
||||
|
||||
if (t.IsFaulted && JobStatus.Status != EJobStatus.Canceled) {
|
||||
JobStatus.Fail($"Unhandled exception: {t.Exception?.InnerException?.Message}");
|
||||
}
|
||||
@@ -70,6 +85,7 @@ public abstract class Job {
|
||||
Completed?.Invoke(this, JobStatus);
|
||||
break;
|
||||
}
|
||||
cts?.Dispose();
|
||||
Done?.Invoke(this, JobStatus);
|
||||
}
|
||||
);
|
||||
@@ -85,9 +101,18 @@ public abstract class Job {
|
||||
protected abstract Task TaskJob(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the job if it's running. Canceled event will be called by the callbacks on the task.
|
||||
/// Cancels the job. For running jobs, signals the cancellation token. For queued jobs,
|
||||
/// marks the status as Canceled and fires completion events directly.
|
||||
/// </summary>
|
||||
public virtual void Cancel() => cts?.Cancel();
|
||||
public virtual void Cancel() {
|
||||
if (cts is not null) {
|
||||
cts.Cancel();
|
||||
} else {
|
||||
JobStatus.Cancel();
|
||||
Canceled?.Invoke(this, JobStatus);
|
||||
Done?.Invoke(this, JobStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the job starts running.
|
||||
@@ -121,4 +146,22 @@ public abstract class Job {
|
||||
/// Raised when the job is done, regardless of the final status (Completed, Canceled, or Failed).
|
||||
/// </summary>
|
||||
public virtual event EventHandler<JobStatus>? Done;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the StartedWaiting event.
|
||||
/// </summary>
|
||||
/// <param name="e">The job status.</param>
|
||||
protected virtual void OnStartedWaiting(JobStatus e) { StartedWaiting?.Invoke(this, e); }
|
||||
|
||||
/// <summary>
|
||||
/// Raises the FinishedWaiting event.
|
||||
/// </summary>
|
||||
/// <param name="e">The job status.</param>
|
||||
protected virtual void OnFinishedWaiting(JobStatus e) { FinishedWaiting?.Invoke(this, e); }
|
||||
|
||||
/// <summary>
|
||||
/// Raises the ProgressChanged event.
|
||||
/// </summary>
|
||||
/// <param name="e">The job status.</param>
|
||||
protected virtual void OnProgressChanged(JobStatus e) { ProgressChanged?.Invoke(this, e); }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Butter.Dtos.Jobs;
|
||||
using Butter.Settings;
|
||||
using Butter.Types;
|
||||
using Lactose.Mapper;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Utils;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
@@ -45,25 +45,35 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
|
||||
/// <summary>
|
||||
/// Returns children of a given parent job — checks active jobs first, then the database.
|
||||
/// Supports delta sync via the <paramref name="since"/> parameter.
|
||||
/// </summary>
|
||||
public List<JobStatusDto> GetChildren(Guid parentId) {
|
||||
var results = new List<JobStatusDto>();
|
||||
/// <param name="parentId">The parent job ID.</param>
|
||||
/// <param name="since">If set, only children changed after this timestamp are returned. Null returns all children.</param>
|
||||
public List<JobStatusDto> GetChildren(Guid parentId, DateTime? since) {
|
||||
var seenIds = new HashSet<Guid>();
|
||||
|
||||
// Active children still in memory
|
||||
foreach (var job in activeJobs.Values) {
|
||||
if (job.ParentJobId == parentId) {
|
||||
results.Add(job.ToJobStatusDto());
|
||||
}
|
||||
var active = activeJobs.Values
|
||||
.Where(j => j.ParentJobId == parentId)
|
||||
.ToList();
|
||||
|
||||
foreach (var job in active) {
|
||||
seenIds.Add(job.Id);
|
||||
}
|
||||
|
||||
var results = active
|
||||
.Where(j => since is null || j.JobStatus.LastChange > since!.Value)
|
||||
.Select(j => j.ToJobStatusDto())
|
||||
.ToList();
|
||||
|
||||
// Past children in the database
|
||||
try {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||
var records = repo.GetChildren(parentId);
|
||||
foreach (var record in records) {
|
||||
results.Add(record.ToJobStatusDto());
|
||||
}
|
||||
var records = since is null
|
||||
? repo.GetChildren(parentId)
|
||||
: repo.GetChildrenModifiedSince(parentId, since.Value);
|
||||
results.AddRange(records.Where(r => !seenIds.Contains(r.Id)).Select(r => r.ToJobStatusDto()));
|
||||
} catch (Exception ex) {
|
||||
logger.LogWarning(ex, "Could not load children for parent {ParentId}", parentId);
|
||||
}
|
||||
@@ -183,6 +193,19 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
||||
logger.LogWarning(ex, "Could not load past jobs from database.");
|
||||
}
|
||||
|
||||
// Load MaxConcurrentJobs from the database setting
|
||||
try {
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var settingsRepo = scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
var maxJobsSetting = settingsRepo.Get(Settings.MaxConcurrentJobs.AsString());
|
||||
if (maxJobsSetting != null && int.TryParse(maxJobsSetting.Value, out var maxJobs) && maxJobs > 0) {
|
||||
MaxConcurrentJobs = maxJobs;
|
||||
logger.LogInformation("Loaded MaxConcurrentJobs from settings: {Value}", MaxConcurrentJobs);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.LogWarning(ex, "Could not load MaxConcurrentJobs setting; using default {Default}.", MaxConcurrentJobs);
|
||||
}
|
||||
|
||||
var lifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
var service = new Task(() => {
|
||||
|
||||
@@ -38,6 +38,11 @@ public class JobStatus(Job job) {
|
||||
/// Gets the progress of the job as a value between 0 and 1.
|
||||
/// </summary>
|
||||
public float Progress { get; private set; } = 0;
|
||||
/// <summary>
|
||||
/// Gets the timestamp of the last change to this job's status or progress.
|
||||
/// Used for delta-based children sync.
|
||||
/// </summary>
|
||||
public DateTime LastChange { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the job status.
|
||||
@@ -49,10 +54,11 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional start message.</param>
|
||||
public void Start(string message = "Job started") {
|
||||
Status = EJobStatus.Running;
|
||||
Started = DateTime.UtcNow;
|
||||
Progress = 0;
|
||||
Message = message;
|
||||
Status = EJobStatus.Running;
|
||||
Started = DateTime.UtcNow;
|
||||
Progress = 0;
|
||||
Message = message;
|
||||
LastChange = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,8 +66,9 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional waiting message.</param>
|
||||
public void Wait(string message = "Job waiting") {
|
||||
Status = EJobStatus.Waiting;
|
||||
Message = message;
|
||||
Status = EJobStatus.Waiting;
|
||||
Message = message;
|
||||
LastChange = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,7 +80,8 @@ public class JobStatus(Job job) {
|
||||
if (progress < 0 || progress > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(progress), "Progress must be between 0 and 1");
|
||||
|
||||
Progress = progress;
|
||||
Progress = progress;
|
||||
LastChange = DateTime.UtcNow;
|
||||
if (message != null) Message = message;
|
||||
}
|
||||
|
||||
@@ -82,9 +90,10 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional completion message.</param>
|
||||
public void Complete(string? message = null) {
|
||||
Status = EJobStatus.Completed;
|
||||
Finished = DateTime.UtcNow;
|
||||
Progress = 1;
|
||||
Status = EJobStatus.Completed;
|
||||
Finished = DateTime.UtcNow;
|
||||
Progress = 1;
|
||||
LastChange = DateTime.UtcNow;
|
||||
if (message != null) Message = message;
|
||||
}
|
||||
|
||||
@@ -93,9 +102,10 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional completion message.</param>
|
||||
public void CompleteWithErrors(string? message = null) {
|
||||
Status = EJobStatus.CompletedWithErrors;
|
||||
Finished = DateTime.UtcNow;
|
||||
Progress = 1;
|
||||
Status = EJobStatus.CompletedWithErrors;
|
||||
Finished = DateTime.UtcNow;
|
||||
Progress = 1;
|
||||
LastChange = DateTime.UtcNow;
|
||||
if (message != null) Message = message;
|
||||
}
|
||||
|
||||
@@ -104,8 +114,9 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional failure message.</param>
|
||||
public void Fail(string? message = null) {
|
||||
Status = EJobStatus.Failed;
|
||||
Finished = DateTime.UtcNow;
|
||||
Status = EJobStatus.Failed;
|
||||
Finished = DateTime.UtcNow;
|
||||
LastChange = DateTime.UtcNow;
|
||||
if (message != null) Message = message;
|
||||
}
|
||||
|
||||
@@ -114,8 +125,9 @@ public class JobStatus(Job job) {
|
||||
/// </summary>
|
||||
/// <param name="message">Optional cancellation message.</param>
|
||||
public void Cancel(string? message = null) {
|
||||
Status = EJobStatus.Canceled;
|
||||
Finished = DateTime.UtcNow;
|
||||
Status = EJobStatus.Canceled;
|
||||
Finished = DateTime.UtcNow;
|
||||
LastChange = DateTime.UtcNow;
|
||||
if (message != null) Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Butter.Settings;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using SixLabors.ImageSharp;
|
||||
@@ -21,6 +20,7 @@ public sealed class MetadataJob : Job {
|
||||
int failedAssets;
|
||||
int totalAssets = 1;
|
||||
int batchSize;
|
||||
List<Job> childJobs = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name { get; }
|
||||
@@ -102,9 +102,15 @@ public sealed class MetadataJob : Job {
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
|
||||
var msg = $"Processed {processedAssets}/{Batch.Length} assets, {failedAssets} failed.";
|
||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(msg);
|
||||
JobStatus.Complete(msg);
|
||||
|
||||
if (failedAssets == Batch.Length)
|
||||
JobStatus.Fail($"All {failedAssets:N0} assets in batch failed.");
|
||||
else if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors(msg);
|
||||
else
|
||||
JobStatus.Complete(msg);
|
||||
}
|
||||
|
||||
async Task MasterJob(CancellationToken token) {
|
||||
@@ -139,13 +145,14 @@ public sealed class MetadataJob : Job {
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
var job = JobManager.CreateJob<MetadataJob>(this, batchList, batchSize);
|
||||
job.LinkParentToken(token);
|
||||
lock (childJobs) { childJobs.Add(job); }
|
||||
|
||||
job.Done += (o, _) => {
|
||||
var sub = (MetadataJob)o!;
|
||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
||||
Interlocked.Increment(ref failedAssets);
|
||||
lock (childJobs) { childJobs.Remove(sub); }
|
||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||
failedAssets += sub.failedAssets;
|
||||
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||
if (Interlocked.Decrement(ref pending) == 0)
|
||||
childTcs.TrySetResult();
|
||||
};
|
||||
@@ -155,7 +162,7 @@ public sealed class MetadataJob : Job {
|
||||
|
||||
JobStatus.UpdateProgress(
|
||||
(float)offset / totalAssets,
|
||||
$"Created batch at {offset}/{totalAssets}."
|
||||
$"Created batch at {offset:N0}/{totalAssets:N0}."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,7 +185,7 @@ public sealed class MetadataJob : Job {
|
||||
}
|
||||
|
||||
if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors($"{failedAssets} of {totalAssets} metadata extractions failed.");
|
||||
JobStatus.CompleteWithErrors($"{failedAssets:N0} of {totalAssets:N0} metadata extractions failed.");
|
||||
else
|
||||
JobStatus.Complete("All metadata extracted successfully.");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Butter.Settings;
|
||||
using Butter.Types;
|
||||
using CoenM.ImageHash.HashAlgorithms;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using SixLabors.ImageSharp;
|
||||
using CoenM.ImageHash.HashAlgorithms;
|
||||
using Lactose.Utils;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Collections;
|
||||
|
||||
@@ -24,6 +23,7 @@ public sealed class PHashJob : Job {
|
||||
int failedAssets;
|
||||
int totalAssets = 1;
|
||||
int batchSize;
|
||||
List<Job> childJobs = [];
|
||||
ILogger<PHashJob> Logger { get; }
|
||||
/// <inheritdoc />
|
||||
public override string Name { get; }
|
||||
@@ -112,9 +112,15 @@ public sealed class PHashJob : Job {
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
|
||||
var msg = $"Processed {processedAssets}/{Batch.Length} assets, {failedAssets} failed.";
|
||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(msg);
|
||||
JobStatus.Complete(msg);
|
||||
|
||||
if (failedAssets == Batch.Length)
|
||||
JobStatus.Fail($"All {failedAssets:N0} assets in batch failed.");
|
||||
else if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors(msg);
|
||||
else
|
||||
JobStatus.Complete(msg);
|
||||
}
|
||||
|
||||
async Task MasterJob(CancellationToken token) {
|
||||
@@ -149,13 +155,14 @@ public sealed class PHashJob : Job {
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
var job = JobManager.CreateJob<PHashJob>(this, batchList, batchSize);
|
||||
job.LinkParentToken(token);
|
||||
lock (childJobs) { childJobs.Add(job); }
|
||||
|
||||
job.Done += (o, _) => {
|
||||
var sub = (PHashJob)o!;
|
||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
||||
Interlocked.Increment(ref failedAssets);
|
||||
lock (childJobs) { childJobs.Remove(sub); }
|
||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||
failedAssets += sub.failedAssets;
|
||||
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||
if (Interlocked.Decrement(ref pending) == 0)
|
||||
childTcs.TrySetResult();
|
||||
};
|
||||
@@ -165,7 +172,7 @@ public sealed class PHashJob : Job {
|
||||
|
||||
JobStatus.UpdateProgress(
|
||||
(float)offset / totalAssets,
|
||||
$"Created batch at {offset}/{totalAssets}."
|
||||
$"Created batch at {offset:N0}/{totalAssets:N0}."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +195,7 @@ public sealed class PHashJob : Job {
|
||||
}
|
||||
|
||||
if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors($"{failedAssets} of {totalAssets} pHash calculations failed.");
|
||||
JobStatus.CompleteWithErrors($"{failedAssets:N0} of {totalAssets:N0} pHash calculations failed.");
|
||||
else
|
||||
JobStatus.Complete("All pHash calculations completed.");
|
||||
}
|
||||
|
||||
294
Lactose/Jobs/PreviewJob.cs
Normal file
294
Lactose/Jobs/PreviewJob.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using Butter.Settings;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Generates WebP preview images for assets missing them, with wrong dimensions, or wrong format.
|
||||
/// Master creates one sub-job per batch of assets.
|
||||
/// </summary>
|
||||
public class PreviewJob : Job {
|
||||
PreviewJob? ParentJob;
|
||||
Asset[]? Batch;
|
||||
int PreviewSize;
|
||||
int PreviewQuality;
|
||||
string? PreviewPath;
|
||||
string ExpectedFormat = "webp";
|
||||
int processedAssets;
|
||||
int failedAssets;
|
||||
int totalAssets = 1;
|
||||
int batchSize;
|
||||
List<Job> childJobs = [];
|
||||
ILogger<PreviewJob> Logger;
|
||||
ISettingsRepository SettingsRepository;
|
||||
IAssetRepository AssetRepository;
|
||||
JobManager? JobManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a master preview generation job.
|
||||
/// </summary>
|
||||
public PreviewJob(
|
||||
ILogger<PreviewJob> logger,
|
||||
ISettingsRepository settingsRepository,
|
||||
IAssetRepository assetRepository,
|
||||
JobManager jobManager
|
||||
) {
|
||||
Logger = logger;
|
||||
SettingsRepository = settingsRepository;
|
||||
AssetRepository = assetRepository;
|
||||
JobManager = jobManager;
|
||||
Name = "Preview Generation Job";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sub-job to generate previews for a batch of assets.
|
||||
/// </summary>
|
||||
public PreviewJob(
|
||||
PreviewJob parentJob,
|
||||
IEnumerable<Asset> batch,
|
||||
int previewSize,
|
||||
string previewPath,
|
||||
int previewQuality,
|
||||
string expectedFormat,
|
||||
int batchSize,
|
||||
ILogger<PreviewJob> logger,
|
||||
ISettingsRepository settingsRepository,
|
||||
IAssetRepository assetRepository
|
||||
) {
|
||||
Logger = logger;
|
||||
SettingsRepository = settingsRepository;
|
||||
AssetRepository = assetRepository;
|
||||
Batch = batch.ToArray();
|
||||
Name = $"Preview Batch ({Batch.Length} assets)";
|
||||
ParentJob = parentJob;
|
||||
ParentJobId = parentJob.Id;
|
||||
PreviewSize = previewSize;
|
||||
PreviewPath = previewPath;
|
||||
PreviewQuality = previewQuality;
|
||||
ExpectedFormat = expectedFormat;
|
||||
this.batchSize = batchSize;
|
||||
totalAssets = Batch.Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task TaskJob(CancellationToken token) {
|
||||
if (ParentJob == null) {
|
||||
await MasterJob(token);
|
||||
} else {
|
||||
SlaveJob(token);
|
||||
}
|
||||
}
|
||||
|
||||
void SlaveJob(CancellationToken token) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Batch == null || Batch.Length == 0) {
|
||||
Logger.LogWarning("Sub-job started with an empty batch. Completing.");
|
||||
JobStatus.Complete("Empty batch — nothing to process.");
|
||||
return;
|
||||
}
|
||||
|
||||
JobStatus.Start();
|
||||
|
||||
if (PreviewPath == null) {
|
||||
Logger.LogError("Preview path is not set. Cannot save previews.");
|
||||
JobStatus.Fail("Preview path is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < Batch.Length; i++) {
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
}
|
||||
|
||||
var asset = Batch[i];
|
||||
JobStatus.UpdateProgress((float)i / Batch.Length, $"Processing {asset.OriginalPath}");
|
||||
|
||||
try {
|
||||
if (string.IsNullOrEmpty(asset.OriginalPath) || !File.Exists(asset.OriginalPath)) {
|
||||
var msg = $"Original file for asset ID {asset.Id} not found at path {asset.OriginalPath}";
|
||||
Logger.LogWarning(msg);
|
||||
failedAssets++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(asset.PreviewPath) && File.Exists(asset.PreviewPath)) {
|
||||
Logger.LogInformation("Removing stale preview for asset ID {Id} at {Path}.", asset.Id, asset.PreviewPath);
|
||||
File.Delete(asset.PreviewPath);
|
||||
asset.PreviewPath = "";
|
||||
}
|
||||
|
||||
using var image = Image.Load(asset.OriginalPath);
|
||||
|
||||
image.Mutate(x => x.Resize(
|
||||
new ResizeOptions {
|
||||
Size = new Size(PreviewSize),
|
||||
Mode = ResizeMode.Max,
|
||||
Sampler = KnownResamplers.CatmullRom
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
var path = PathFromGuid(asset.Id, PreviewPath!);
|
||||
|
||||
if (!Directory.Exists(Path.GetDirectoryName(path)))
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
image.SaveAsWebp(
|
||||
path,
|
||||
new WebpEncoder() {
|
||||
Quality = PreviewQuality,
|
||||
SkipMetadata = true
|
||||
}
|
||||
);
|
||||
|
||||
asset.PreviewPath = path;
|
||||
asset.PreviewSize = PreviewSize;
|
||||
asset.PreviewFormat = ExpectedFormat;
|
||||
processedAssets++;
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, $"Failed to generate preview for asset ID {asset.Id}");
|
||||
failedAssets++;
|
||||
}
|
||||
}
|
||||
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
|
||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(resultMsg);
|
||||
|
||||
if (failedAssets == Batch.Length)
|
||||
JobStatus.Fail($"All {failedAssets:N0} assets in batch failed.");
|
||||
else if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors(resultMsg);
|
||||
else
|
||||
JobStatus.Complete(resultMsg);
|
||||
}
|
||||
|
||||
async Task MasterJob(CancellationToken token) {
|
||||
if (JobManager == null) {
|
||||
Logger.LogError("JobManager is not available. Cannot create sub-jobs.");
|
||||
JobStatus.Fail("JobManager is not available. Cannot create sub-jobs.");
|
||||
return;
|
||||
}
|
||||
|
||||
JobStatus.Start();
|
||||
Logger.LogInformation("Starting master Preview job.");
|
||||
|
||||
var previewPathSetting = SettingsRepository.Get(Settings.PreviewPath.AsString());
|
||||
var previewSizeSetting = SettingsRepository.Get(Settings.PreviewSize.AsString());
|
||||
var previewQualitySetting = SettingsRepository.Get(Settings.PreviewQuality.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
|
||||
if (previewPathSetting == null) {
|
||||
Logger.LogError("Preview path not found. Cannot proceed.");
|
||||
JobStatus.Fail("Preview path not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(previewSizeSetting?.Value, out var previewSize)) {
|
||||
previewSize = 2048;
|
||||
Logger.LogWarning("Preview Size setting not found or invalid. Defaulting to {Size}.", previewSize);
|
||||
}
|
||||
|
||||
if (!int.TryParse(previewQualitySetting?.Value, out var previewQuality)) {
|
||||
previewQuality = 85;
|
||||
Logger.LogWarning("Preview Quality setting not found or invalid. Defaulting to {Quality}.", previewQuality);
|
||||
}
|
||||
|
||||
previewQuality = Math.Clamp(previewQuality, 1, 100);
|
||||
|
||||
batchSize = 200;
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingOrWrongPreview(previewSize, ExpectedFormat);
|
||||
Logger.LogInformation("Found {Count} assets needing previews.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
JobStatus.Complete("No assets need previews.");
|
||||
return;
|
||||
}
|
||||
|
||||
var childTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingOrWrongPreview(previewSize, ExpectedFormat, batchSize, offset);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
var job = JobManager.CreateJob<PreviewJob>(
|
||||
this, batchList, previewSize, previewPathSetting.Value!, previewQuality, ExpectedFormat, batchSize
|
||||
);
|
||||
job.LinkParentToken(token);
|
||||
lock (childJobs) { childJobs.Add(job); }
|
||||
|
||||
int capturedOffset = offset;
|
||||
job.Done += (o, _) => {
|
||||
var sub = (PreviewJob)o!;
|
||||
lock (childJobs) { childJobs.Remove(sub); }
|
||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||
if (Interlocked.Decrement(ref pending) == 0)
|
||||
childTcs.TrySetResult();
|
||||
};
|
||||
|
||||
Interlocked.Increment(ref pending);
|
||||
JobManager.EnqueueJob(job);
|
||||
|
||||
JobStatus.UpdateProgress(
|
||||
(float)offset / totalAssets,
|
||||
$"Created batch at {offset:N0}/{totalAssets:N0}."
|
||||
);
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
}
|
||||
|
||||
var remain = Interlocked.CompareExchange(ref pending, 0, 0);
|
||||
if (remain == 0) {
|
||||
childTcs.TrySetResult();
|
||||
} else {
|
||||
using var ctr = token.Register(() => childTcs.TrySetCanceled(token));
|
||||
try {
|
||||
await childTcs.Task;
|
||||
} catch (TaskCanceledException) {
|
||||
JobStatus.Cancel("Cancellation requested from user.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors($"{failedAssets:N0} of {totalAssets:N0} previews failed.");
|
||||
else
|
||||
JobStatus.Complete("All previews generated successfully.");
|
||||
}
|
||||
|
||||
static string PathFromGuid(Guid id, string root) {
|
||||
var s = id.ToString("N");
|
||||
|
||||
return Path.Combine(
|
||||
root,
|
||||
s[..2],
|
||||
s.Substring(2, 2),
|
||||
s.Substring(4, 2),
|
||||
$"{s}.webp"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
using Butter.Dtos.Settings;
|
||||
using Butter.Settings;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Lactose.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Generates JPEG thumbnails for assets missing them or with wrong dimensions.
|
||||
/// Generates WebP thumbnails for assets missing them, with wrong dimensions, or wrong format.
|
||||
/// Master creates one sub-job per batch of assets.
|
||||
/// </summary>
|
||||
public class ThumbnailJob : Job {
|
||||
ThumbnailJob? ParentJob;
|
||||
Asset[]? Batch;
|
||||
int ThumbnailSize;
|
||||
int ThumbnailQuality;
|
||||
string? ThumbnailPath;
|
||||
string ExpectedFormat = "webp";
|
||||
int processedAssets;
|
||||
int failedAssets;
|
||||
int totalAssets = 1;
|
||||
int batchSize;
|
||||
List<Job> childJobs = [];
|
||||
ILogger<ThumbnailJob> Logger;
|
||||
ISettingsRepository SettingsRepository;
|
||||
IAssetRepository AssetRepository;
|
||||
@@ -54,6 +55,8 @@ public class ThumbnailJob : Job {
|
||||
IEnumerable<Asset> batch,
|
||||
int thumbnailSize,
|
||||
string thumbnailPath,
|
||||
int thumbnailQuality,
|
||||
string expectedFormat,
|
||||
int batchSize,
|
||||
ILogger<ThumbnailJob> logger,
|
||||
ISettingsRepository settingsRepository,
|
||||
@@ -68,6 +71,8 @@ public class ThumbnailJob : Job {
|
||||
ParentJobId = parentJob.Id;
|
||||
ThumbnailSize = thumbnailSize;
|
||||
ThumbnailPath = thumbnailPath;
|
||||
ThumbnailQuality = thumbnailQuality;
|
||||
ExpectedFormat = expectedFormat;
|
||||
this.batchSize = batchSize;
|
||||
totalAssets = Batch.Length;
|
||||
}
|
||||
@@ -140,17 +145,17 @@ public class ThumbnailJob : Job {
|
||||
if (!Directory.Exists(Path.GetDirectoryName(path)))
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
image.SaveAsJpeg(
|
||||
image.SaveAsWebp(
|
||||
path,
|
||||
new JpegEncoder() {
|
||||
ColorType = JpegEncodingColor.Rgb,
|
||||
Quality = 75,
|
||||
new WebpEncoder() {
|
||||
Quality = ThumbnailQuality,
|
||||
SkipMetadata = true
|
||||
}
|
||||
);
|
||||
|
||||
asset.ThumbnailPath = path;
|
||||
asset.ThumbnailSize = ThumbnailSize;
|
||||
asset.ThumbnailPath = path;
|
||||
asset.ThumbnailSize = ThumbnailSize;
|
||||
asset.ThumbnailFormat = ExpectedFormat;
|
||||
processedAssets++;
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, $"Failed to generate thumbnail for asset ID {asset.Id}");
|
||||
@@ -161,9 +166,15 @@ public class ThumbnailJob : Job {
|
||||
AssetRepository.UpdateBulk(Batch);
|
||||
AssetRepository.Save();
|
||||
|
||||
var resultMsg = $"Processed {processedAssets}/{Batch.Length} assets, {failedAssets} failed.";
|
||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||
Logger.LogInformation(resultMsg);
|
||||
JobStatus.Complete(resultMsg);
|
||||
|
||||
if (failedAssets == Batch.Length)
|
||||
JobStatus.Fail($"All {failedAssets:N0} assets in batch failed.");
|
||||
else if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors(resultMsg);
|
||||
else
|
||||
JobStatus.Complete(resultMsg);
|
||||
}
|
||||
|
||||
async Task MasterJob(CancellationToken token) {
|
||||
@@ -176,9 +187,10 @@ public class ThumbnailJob : Job {
|
||||
JobStatus.Start();
|
||||
Logger.LogInformation("Starting master Thumbnail job.");
|
||||
|
||||
var thumbPathSetting = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
|
||||
var thumbSizeSetting = SettingsRepository.Get(Settings.ThumbnailSize.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
var thumbPathSetting = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
|
||||
var thumbSizeSetting = SettingsRepository.Get(Settings.ThumbnailSize.AsString());
|
||||
var thumbQualitySetting = SettingsRepository.Get(Settings.ThumbnailQuality.AsString());
|
||||
var batchSizeSetting = SettingsRepository.Get(Settings.JobBatchSize.AsString());
|
||||
|
||||
if (thumbPathSetting == null) {
|
||||
Logger.LogError("Thumbnail path not found. Cannot proceed.");
|
||||
@@ -191,11 +203,18 @@ public class ThumbnailJob : Job {
|
||||
Logger.LogWarning("Thumbnail Size setting not found or invalid. Defaulting to {Size}.", thumbnailSize);
|
||||
}
|
||||
|
||||
if (!int.TryParse(thumbQualitySetting?.Value, out var thumbnailQuality)) {
|
||||
thumbnailQuality = 75;
|
||||
Logger.LogWarning("Thumbnail Quality setting not found or invalid. Defaulting to {Quality}.", thumbnailQuality);
|
||||
}
|
||||
|
||||
thumbnailQuality = Math.Clamp(thumbnailQuality, 1, 100);
|
||||
|
||||
batchSize = 200;
|
||||
if (batchSizeSetting != null && int.TryParse(batchSizeSetting.Value, out var parsed))
|
||||
batchSize = Math.Max(1, parsed);
|
||||
|
||||
totalAssets = AssetRepository.CountAssetsMissingOrWrongThumbnail(thumbnailSize);
|
||||
totalAssets = AssetRepository.CountAssetsMissingOrWrongThumbnail(thumbnailSize, ExpectedFormat);
|
||||
Logger.LogInformation("Found {Count} assets needing thumbnails.", totalAssets);
|
||||
|
||||
if (totalAssets == 0) {
|
||||
@@ -207,22 +226,23 @@ public class ThumbnailJob : Job {
|
||||
int pending = 0;
|
||||
|
||||
for (int offset = 0; offset < totalAssets && !token.IsCancellationRequested; offset += batchSize) {
|
||||
var batch = AssetRepository.GetAssetsMissingOrWrongThumbnail(thumbnailSize, batchSize, offset);
|
||||
var batch = AssetRepository.GetAssetsMissingOrWrongThumbnail(thumbnailSize, ExpectedFormat, batchSize, offset);
|
||||
var batchList = batch.ToList();
|
||||
|
||||
if (batchList.Count == 0) continue;
|
||||
|
||||
var job = JobManager.CreateJob<ThumbnailJob>(
|
||||
this, batchList, thumbnailSize, thumbPathSetting.Value!, batchSize
|
||||
this, batchList, thumbnailSize, thumbPathSetting.Value!, thumbnailQuality, ExpectedFormat, batchSize
|
||||
);
|
||||
job.LinkParentToken(token);
|
||||
lock (childJobs) { childJobs.Add(job); }
|
||||
|
||||
int capturedOffset = offset;
|
||||
job.Done += (o, _) => {
|
||||
var sub = (ThumbnailJob)o!;
|
||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
||||
Interlocked.Increment(ref failedAssets);
|
||||
lock (childJobs) { childJobs.Remove(sub); }
|
||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||
failedAssets += sub.failedAssets;
|
||||
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||
if (Interlocked.Decrement(ref pending) == 0)
|
||||
childTcs.TrySetResult();
|
||||
};
|
||||
@@ -232,7 +252,7 @@ public class ThumbnailJob : Job {
|
||||
|
||||
JobStatus.UpdateProgress(
|
||||
(float)offset / totalAssets,
|
||||
$"Created batch at {offset}/{totalAssets}."
|
||||
$"Created batch at {offset:N0}/{totalAssets:N0}."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,7 +275,7 @@ public class ThumbnailJob : Job {
|
||||
}
|
||||
|
||||
if (failedAssets > 0)
|
||||
JobStatus.CompleteWithErrors($"{failedAssets} of {totalAssets} thumbnails failed.");
|
||||
JobStatus.CompleteWithErrors($"{failedAssets:N0} of {totalAssets:N0} thumbnails failed.");
|
||||
else
|
||||
JobStatus.Complete("All thumbnails generated successfully.");
|
||||
}
|
||||
@@ -268,7 +288,7 @@ public class ThumbnailJob : Job {
|
||||
s[..2],
|
||||
s.Substring(2, 2),
|
||||
s.Substring(4, 2),
|
||||
$"{s}.jpg"
|
||||
$"{s}.webp"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<UserSecretsId>00b763fc-028f-4464-9478-fc1006aeffa8</UserSecretsId>
|
||||
<LangVersion>12</LangVersion>
|
||||
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.15">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -35,13 +35,19 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\"/>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Butter\Butter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lactose.Analyzers\Lactose.Analyzers.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="DefaultSettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Butter.Dtos.Album;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
|
||||
namespace Lactose.Mapper;
|
||||
@@ -7,31 +8,46 @@ namespace Lactose.Mapper;
|
||||
/// provides mapping methods for converting Album objects to DTOs.
|
||||
/// </summary>
|
||||
public static class AlbumMapper {
|
||||
/// <summary>
|
||||
/// Maps an Album object to an AlbumPreviewDto with visibility-aware fields.
|
||||
/// </summary>
|
||||
/// <param name="album">The album to map.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <returns>An AlbumPreviewDto object.</returns>
|
||||
public static AlbumPreviewDto ToAlbumPreviewDto(this Album album, EAccessLevel accessLevel) => new AlbumPreviewDto {
|
||||
Id = album.Id,
|
||||
Name = album.Title,
|
||||
Person = album.PersonOwnerId,
|
||||
PersonName = album.PersonOwner?.Name,
|
||||
CoverAssetId = album.CoverAssetId,
|
||||
AssetCount = album.Assets?.Count(a => a.DeletedAt == null) ?? 0,
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? album.Visibility : null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps an Album object to an AlbumFullDto object.
|
||||
/// </summary>
|
||||
/// <param name="album">The album to map.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <returns>An AlbumFullDto object.</returns>
|
||||
public static AlbumFullDto ToAlbumFullDto(this Album album) => new AlbumFullDto {
|
||||
Id = album.Id,
|
||||
Name = album.Title,
|
||||
Owner = album.UserOwnerId,
|
||||
Person = album.PersonOwnerId,
|
||||
Images = album.Assets!.Select(asset => asset.Id).ToList()
|
||||
public static AlbumFullDto ToAlbumFullDto(this Album album, EAccessLevel accessLevel) => new AlbumFullDto {
|
||||
Id = album.Id,
|
||||
Name = album.Title,
|
||||
Person = album.PersonOwnerId,
|
||||
PersonName = album.PersonOwner?.Name,
|
||||
CoverAssetId = album.CoverAssetId,
|
||||
AssetCount = album.Assets?.Count(a => a.DeletedAt == null) ?? 0,
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? album.Visibility : null,
|
||||
Images = album.Assets?.Select(asset => asset.Id).ToList() ?? [],
|
||||
AssetPreviews = album.Assets?.Select(asset => new AlbumAssetPreviewDto {
|
||||
Id = asset.Id,
|
||||
ResolutionWidth = asset.ResolutionWidth,
|
||||
ResolutionHeight = asset.ResolutionHeight,
|
||||
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
||||
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
||||
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null
|
||||
}).ToList() ?? []
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps an Album object to an AlbumPreviewDto object.
|
||||
/// </summary>
|
||||
/// <param name="album">The album to map.</param>
|
||||
/// <returns>An AlbumPreviewDto object.</returns>
|
||||
public static AlbumPreviewDto ToAlbumPreviewDto(this Album album) => new AlbumPreviewDto {
|
||||
Id = album.Id,
|
||||
Name = album.Title,
|
||||
Owner = album.UserOwnerId,
|
||||
Person = album.PersonOwnerId
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Butter.Dtos.Asset;
|
||||
using Butter.Dtos.User;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
|
||||
@@ -9,43 +10,65 @@ namespace Lactose.Mapper;
|
||||
/// </summary>
|
||||
public static class AssetsMapper {
|
||||
/// <summary>
|
||||
/// Maps an Asset object to a GetFullAssetDto object.
|
||||
/// Maps an Asset object to a AssetDto object.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to map.</param>
|
||||
/// <param name="accessLevel">The access level of the user.</param>
|
||||
/// <returns>A GetFullAssetDto object.</returns>
|
||||
public static AssetDto ToFullAssetsDto(this Asset asset, EAccessLevel accessLevel) => new AssetDto {
|
||||
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||
/// <returns>A AssetDto object.</returns>
|
||||
public static AssetDto ToFullAssetsDto(this Asset asset, EAccessLevel accessLevel, Guid? viewerId) => new AssetDto {
|
||||
Id = asset.Id,
|
||||
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||
AssetType = asset.Type,
|
||||
IsPublic = asset.IsPubliclyShared,
|
||||
CreatedAt = asset.CreatedAt,
|
||||
UpdatedAt = asset.UpdatedAt,
|
||||
DeletedAt = asset.DeletedAt,
|
||||
Owner = asset.Owner!.ToGetUsersDto(),
|
||||
UploadedBy = asset.Uploader?.ToGetUsersDto(accessLevel, viewerId)
|
||||
?? new UserInfoDto { Id = Guid.Empty, Username = "deleted", AccessLevel = EAccessLevel.User },
|
||||
MimeType = asset.MimeType,
|
||||
ResolutionWidth = asset.ResolutionWidth,
|
||||
ResolutionHeight = asset.ResolutionHeight,
|
||||
FileSize = asset.FileSize,
|
||||
Duration = asset.Duration ?? 0,
|
||||
FrameRate = asset.FrameRate ?? 0
|
||||
FrameRate = asset.FrameRate ?? 0,
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps an Asset object to a GetAssetPreviewDto object.
|
||||
/// Maps an Asset object to a AssetPreviewDto object.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to map.</param>
|
||||
/// <returns>A GetAssetPreviewDto object.</returns>
|
||||
public static AssetPreviewDto ToAssetPreviewDto(this Asset asset) => new AssetPreviewDto {
|
||||
Id = asset.Id,
|
||||
MimeType = asset.MimeType
|
||||
/// <param name="accessLevel">The access level of the user.</param>
|
||||
/// <returns>A AssetPreviewDto object.</returns>
|
||||
public static AssetPreviewDto ToAssetPreviewDto(this Asset asset, EAccessLevel accessLevel) => new AssetPreviewDto {
|
||||
Id = asset.Id,
|
||||
MimeType = asset.MimeType,
|
||||
ResolutionWidth = asset.ResolutionWidth,
|
||||
ResolutionHeight = asset.ResolutionHeight,
|
||||
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
||||
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
||||
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||
AlbumNames = asset.Albums?.Select(a => a.Title).Distinct().ToList(),
|
||||
AlbumIds = asset.Albums?.Select(a => a.Id).Distinct().ToList(),
|
||||
CosplayerNames = asset.Albums?
|
||||
.Where(a => a.PersonOwner != null)
|
||||
.Select(a => a.PersonOwner!.Name)
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
CosplayerIds = asset.Albums?
|
||||
.Where(a => a.PersonOwner != null)
|
||||
.Select(a => a.PersonOwner!.Id)
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps a collection of Asset objects to a collection of GetAssetPreviewDto objects.
|
||||
/// Maps a collection of Asset objects to a collection of AssetPreviewDto objects.
|
||||
/// </summary>
|
||||
/// <param name="assets">The collection of assets to map.</param>
|
||||
/// <returns>A collection of GetAssetPreviewDto objects.</returns>
|
||||
public static IEnumerable<AssetPreviewDto> ToAssetPreviewDto(this IEnumerable<Asset> assets)
|
||||
=> assets.Select(ToAssetPreviewDto);
|
||||
/// <param name="accessLevel">The access level of the user.</param>
|
||||
/// <returns>A collection of AssetPreviewDto objects.</returns>
|
||||
public static IEnumerable<AssetPreviewDto> ToAssetPreviewDto(this IEnumerable<Asset> assets, EAccessLevel accessLevel)
|
||||
=> assets.Select(a => a.ToAssetPreviewDto(accessLevel));
|
||||
}
|
||||
@@ -26,9 +26,12 @@ public static class JobMapper {
|
||||
JobType = job switch {
|
||||
FileSystemCrawlJob => EJobType.FileSystemScan,
|
||||
ThumbnailJob => EJobType.ThumbnailGeneration,
|
||||
PreviewJob => EJobType.PreviewGeneration,
|
||||
IntegrityCheckJob => EJobType.IntegrityCheck,
|
||||
PHashJob => EJobType.PHashGeneration,
|
||||
MetadataJob => EJobType.MetadataExtraction,
|
||||
CreatePersonsJob => EJobType.CreatePersons,
|
||||
CreateAlbumsJob => EJobType.CreateAlbums,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(job))
|
||||
}
|
||||
};
|
||||
|
||||
54
Lactose/Mapper/PersonMapper.cs
Normal file
54
Lactose/Mapper/PersonMapper.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Butter.Dtos.Album;
|
||||
using Butter.Dtos.Person;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
|
||||
namespace Lactose.Mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Provides mapping methods for converting Person objects to DTOs.
|
||||
/// </summary>
|
||||
public static class PersonMapper {
|
||||
/// <summary>
|
||||
/// Maps a Person to a PersonDetailedDto with album previews and stats.
|
||||
/// </summary>
|
||||
/// <param name="person">The person to map.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <returns>A PersonDetailedDto.</returns>
|
||||
public static PersonDetailedDto ToPersonDetailedDto(this Person person, EAccessLevel accessLevel) {
|
||||
var albums = person.Albums ?? [];
|
||||
var maintainers = person.Maintainers ?? [];
|
||||
|
||||
return new PersonDetailedDto {
|
||||
Id = person.Id,
|
||||
Name = person.Name,
|
||||
ProfileAssetId = person.ProfileAssetId,
|
||||
ProfileCropX = person.ProfileCropX,
|
||||
ProfileCropY = person.ProfileCropY,
|
||||
ProfileCropZoom = person.ProfileCropZoom,
|
||||
TotalAlbums = albums.Count,
|
||||
TotalAssets = albums.Sum(a => a.Assets?.Count(asset => asset.DeletedAt == null) ?? 0),
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? person.Visibility : null,
|
||||
Albums = albums.Select(a => a.ToAlbumPreviewDto(accessLevel)).ToList(),
|
||||
MaintainerUserIds = maintainers.Select(u => u.Id).ToList(),
|
||||
MaintainerUsernames = maintainers.Select(u => u.Username).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a Person to a PersonPreviewDto.
|
||||
/// </summary>
|
||||
/// <param name="person">The person to map.</param>
|
||||
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||
/// <returns>A PersonPreviewDto.</returns>
|
||||
public static PersonPreviewDto ToPersonPreviewDto(this Person person, EAccessLevel accessLevel) => new() {
|
||||
Id = person.Id,
|
||||
Name = person.Name,
|
||||
ProfileAssetId = person.ProfileAssetId,
|
||||
ProfileCropX = person.ProfileCropX,
|
||||
ProfileCropY = person.ProfileCropY,
|
||||
ProfileCropZoom = person.ProfileCropZoom,
|
||||
TotalAlbums = person.Albums?.Count ?? 0,
|
||||
Visibility = accessLevel >= EAccessLevel.Maintainer ? person.Visibility : null
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Butter.Dtos.User;
|
||||
using Butter.Types;
|
||||
using Lactose.Models;
|
||||
|
||||
namespace Lactose.Mapper;
|
||||
@@ -8,19 +9,33 @@ namespace Lactose.Mapper;
|
||||
/// </summary>
|
||||
public static class UsersMapper {
|
||||
/// <summary>
|
||||
/// Maps a User object to a UserInfoDto object.
|
||||
/// Maps a User object to a UserInfoDto object, filtering sensitive fields based on the viewer's access.
|
||||
/// Sensitive fields (Email, Ban/Delete status, AccessLevel, MaintainedPersons) are only populated
|
||||
/// when the viewer is an admin or viewing their own profile.
|
||||
/// </summary>
|
||||
/// <param name="user">The User object to map.</param>
|
||||
/// <param name="viewerAccessLevel">The access level of the requesting user.</param>
|
||||
/// <param name="viewerId">The ID of the requesting user (null if anonymous).</param>
|
||||
/// <returns>A UserInfoDto object containing the mapped data.</returns>
|
||||
public static UserInfoDto ToGetUsersDto(this User user) {
|
||||
public static UserInfoDto ToGetUsersDto(this User user, EAccessLevel viewerAccessLevel, Guid? viewerId) {
|
||||
bool isAdmin = viewerAccessLevel >= EAccessLevel.Admin;
|
||||
bool isSelf = viewerId == user.Id;
|
||||
bool showSensitive = isAdmin || isSelf;
|
||||
|
||||
return new UserInfoDto {
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
CreatedAt = user.CreatedAt,
|
||||
IsBanned = user.BannedAt != null,
|
||||
DeletedAt = user.DeletedAt,
|
||||
AccessLevel = user.AccessLevel,
|
||||
Id = user.Id,
|
||||
Username = user.Username,
|
||||
Email = showSensitive ? user.Email : null,
|
||||
CreatedAt = user.CreatedAt,
|
||||
UpdatedAt = showSensitive ? user.UpdatedAt : null,
|
||||
LastLogin = showSensitive ? user.LastLogin : null,
|
||||
BannedAt = showSensitive ? user.BannedAt : null,
|
||||
DeletedAt = isAdmin ? user.DeletedAt : null,
|
||||
IsBanned = showSensitive && user.BannedAt != null,
|
||||
AccessLevel = showSensitive ? user.AccessLevel : EAccessLevel.User,
|
||||
MaintainedPersonIds = showSensitive ? user.MaintainedPersons?.Select(p => p.Id).ToList() : null,
|
||||
MaintainedPersonNames = showSensitive ? user.MaintainedPersons?.Select(p => p.Name).ToList() : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
533
Lactose/Migrations/20260707160046_FixSharedWithManyToMany.Designer.cs
generated
Normal file
533
Lactose/Migrations/20260707160046_FixSharedWithManyToMany.Designer.cs
generated
Normal file
@@ -0,0 +1,533 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260707160046_FixSharedWithManyToMany")]
|
||||
partial class FixSharedWithManyToMany
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Lactose/Migrations/20260707160046_FixSharedWithManyToMany.cs
Normal file
119
Lactose/Migrations/20260707160046_FixSharedWithManyToMany.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class FixSharedWithManyToMany : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Users_Assets_AssetId",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Users_AssetId",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AssetId",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "JobRecords",
|
||||
type: "VARCHAR(2048)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "VARCHAR(256)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "RegexPattern",
|
||||
table: "Folders",
|
||||
type: "character varying(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "VARCHAR(2048)",
|
||||
oldMaxLength: 2048,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AssetUser",
|
||||
columns: table => new
|
||||
{
|
||||
SharedAssetsId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SharedWithId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AssetUser", x => new { x.SharedAssetsId, x.SharedWithId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AssetUser_Assets_SharedAssetsId",
|
||||
column: x => x.SharedAssetsId,
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AssetUser_Users_SharedWithId",
|
||||
column: x => x.SharedWithId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AssetUser_SharedWithId",
|
||||
table: "AssetUser",
|
||||
column: "SharedWithId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AssetUser");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "AssetId",
|
||||
table: "Users",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "JobRecords",
|
||||
type: "VARCHAR(256)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "VARCHAR(2048)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "RegexPattern",
|
||||
table: "Folders",
|
||||
type: "VARCHAR(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(2048)",
|
||||
oldMaxLength: 2048,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_AssetId",
|
||||
table: "Users",
|
||||
column: "AssetId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Users_Assets_AssetId",
|
||||
table: "Users",
|
||||
column: "AssetId",
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
543
Lactose/Migrations/20260707172403_AddPreviewSizeAndFormatFields.Designer.cs
generated
Normal file
543
Lactose/Migrations/20260707172403_AddPreviewSizeAndFormatFields.Designer.cs
generated
Normal file
@@ -0,0 +1,543 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260707172403_AddPreviewSizeAndFormatFields")]
|
||||
partial class AddPreviewSizeAndFormatFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPreviewSizeAndFormatFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PreviewFormat",
|
||||
table: "Assets",
|
||||
type: "VARCHAR(16)",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PreviewSize",
|
||||
table: "Assets",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ThumbnailFormat",
|
||||
table: "Assets",
|
||||
type: "VARCHAR(16)",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PreviewFormat",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PreviewSize",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ThumbnailFormat",
|
||||
table: "Assets");
|
||||
}
|
||||
}
|
||||
}
|
||||
554
Lactose/Migrations/20260708104055_AddAlbumCoverAsset.Designer.cs
generated
Normal file
554
Lactose/Migrations/20260708104055_AddAlbumCoverAsset.Designer.cs
generated
Normal file
@@ -0,0 +1,554 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260708104055_AddAlbumCoverAsset")]
|
||||
partial class AddAlbumCoverAsset
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Lactose/Migrations/20260708104055_AddAlbumCoverAsset.cs
Normal file
66
Lactose/Migrations/20260708104055_AddAlbumCoverAsset.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbumCoverAsset : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "CoverAssetId",
|
||||
table: "Albums",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Albums_CoverAssetId",
|
||||
table: "Albums",
|
||||
column: "CoverAssetId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Albums_Assets_CoverAssetId",
|
||||
table: "Albums",
|
||||
column: "CoverAssetId",
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "Albums"
|
||||
SET "CoverAssetId" = sub."AssetId"
|
||||
FROM (
|
||||
SELECT DISTINCT ON ("Albums"."Id")
|
||||
"Albums"."Id" AS "AlbumId",
|
||||
"Assets"."Id" AS "AssetId"
|
||||
FROM "Albums"
|
||||
JOIN "AlbumAsset" ON "AlbumAsset"."AlbumsId" = "Albums"."Id"
|
||||
JOIN "Assets" ON "Assets"."Id" = "AlbumAsset"."AssetsId"
|
||||
WHERE "Albums"."CoverAssetId" IS NULL
|
||||
AND "Assets"."DeletedAt" IS NULL
|
||||
ORDER BY "Albums"."Id", "Assets"."OriginalFilename"
|
||||
) AS sub
|
||||
WHERE "Albums"."Id" = sub."AlbumId"
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Albums_Assets_CoverAssetId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Albums_CoverAssetId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverAssetId",
|
||||
table: "Albums");
|
||||
}
|
||||
}
|
||||
}
|
||||
568
Lactose/Migrations/20260709100044_AddPersonProfileAsset.Designer.cs
generated
Normal file
568
Lactose/Migrations/20260709100044_AddPersonProfileAsset.Designer.cs
generated
Normal file
@@ -0,0 +1,568 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260709100044_AddPersonProfileAsset")]
|
||||
partial class AddPersonProfileAsset
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Lactose/Migrations/20260709100044_AddPersonProfileAsset.cs
Normal file
49
Lactose/Migrations/20260709100044_AddPersonProfileAsset.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersonProfileAsset : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProfileAssetId",
|
||||
table: "People",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_People_ProfileAssetId",
|
||||
table: "People",
|
||||
column: "ProfileAssetId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_People_Assets_ProfileAssetId",
|
||||
table: "People",
|
||||
column: "ProfileAssetId",
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_People_Assets_ProfileAssetId",
|
||||
table: "People");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_People_ProfileAssetId",
|
||||
table: "People");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProfileAssetId",
|
||||
table: "People");
|
||||
}
|
||||
}
|
||||
}
|
||||
574
Lactose/Migrations/20260709105956_AddPersonProfileCrop.Designer.cs
generated
Normal file
574
Lactose/Migrations/20260709105956_AddPersonProfileCrop.Designer.cs
generated
Normal file
@@ -0,0 +1,574 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260709105956_AddPersonProfileCrop")]
|
||||
partial class AddPersonProfileCrop
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Lactose/Migrations/20260709105956_AddPersonProfileCrop.cs
Normal file
38
Lactose/Migrations/20260709105956_AddPersonProfileCrop.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersonProfileCrop : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "ProfileCropX",
|
||||
table: "People",
|
||||
type: "real",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "ProfileCropY",
|
||||
table: "People",
|
||||
type: "real",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProfileCropX",
|
||||
table: "People");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProfileCropY",
|
||||
table: "People");
|
||||
}
|
||||
}
|
||||
}
|
||||
577
Lactose/Migrations/20260709111214_AddPersonProfileCropZoom.Designer.cs
generated
Normal file
577
Lactose/Migrations/20260709111214_AddPersonProfileCropZoom.Designer.cs
generated
Normal file
@@ -0,0 +1,577 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260709111214_AddPersonProfileCropZoom")]
|
||||
partial class AddPersonProfileCropZoom
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersonProfileCropZoom : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "ProfileCropZoom",
|
||||
table: "People",
|
||||
type: "real",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProfileCropZoom",
|
||||
table: "People");
|
||||
}
|
||||
}
|
||||
}
|
||||
577
Lactose/Migrations/20260709170106_MigrateToTimestamptz.Designer.cs
generated
Normal file
577
Lactose/Migrations/20260709170106_MigrateToTimestamptz.Designer.cs
generated
Normal file
@@ -0,0 +1,577 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260709170106_MigrateToTimestamptz")]
|
||||
partial class MigrateToTimestamptz
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
144
Lactose/Migrations/20260709170106_MigrateToTimestamptz.cs
Normal file
144
Lactose/Migrations/20260709170106_MigrateToTimestamptz.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MigrateToTimestamptz : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("SET TimeZone='UTC';");
|
||||
|
||||
// Albums
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Albums"" ALTER COLUMN ""CreatedAt"" TYPE timestamp with time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Albums"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp with time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// Assets
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""CreatedAt"" TYPE timestamp with time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp with time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""DeletedAt"" TYPE timestamp with time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// Users
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""CreatedAt"" TYPE timestamp with time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp with time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""LastLogin"" TYPE timestamp with time zone
|
||||
USING ""LastLogin"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""BannedAt"" TYPE timestamp with time zone
|
||||
USING ""BannedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""DeletedAt"" TYPE timestamp with time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""RefreshTokenExpires"" TYPE timestamp with time zone
|
||||
USING ""RefreshTokenExpires"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// People
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""CreatedAt"" TYPE timestamp with time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp with time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""DeletedAt"" TYPE timestamp with time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// JobRecords
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Created"" TYPE timestamp with time zone
|
||||
USING ""Created"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Started"" TYPE timestamp with time zone
|
||||
USING ""Started"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Finished"" TYPE timestamp with time zone
|
||||
USING ""Finished"" AT TIME ZONE 'UTC';");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("SET TimeZone='UTC';");
|
||||
|
||||
// JobRecords
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Finished"" TYPE timestamp without time zone
|
||||
USING ""Finished"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Started"" TYPE timestamp without time zone
|
||||
USING ""Started"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""JobRecords"" ALTER COLUMN ""Created"" TYPE timestamp without time zone
|
||||
USING ""Created"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// People
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""DeletedAt"" TYPE timestamp without time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp without time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""People"" ALTER COLUMN ""CreatedAt"" TYPE timestamp without time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// Users
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""RefreshTokenExpires"" TYPE timestamp without time zone
|
||||
USING ""RefreshTokenExpires"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""DeletedAt"" TYPE timestamp without time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""BannedAt"" TYPE timestamp without time zone
|
||||
USING ""BannedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""LastLogin"" TYPE timestamp without time zone
|
||||
USING ""LastLogin"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp without time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Users"" ALTER COLUMN ""CreatedAt"" TYPE timestamp without time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// Assets
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""DeletedAt"" TYPE timestamp without time zone
|
||||
USING ""DeletedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp without time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Assets"" ALTER COLUMN ""CreatedAt"" TYPE timestamp without time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
|
||||
// Albums
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Albums"" ALTER COLUMN ""UpdatedAt"" TYPE timestamp without time zone
|
||||
USING ""UpdatedAt"" AT TIME ZONE 'UTC';");
|
||||
migrationBuilder.Sql(@"
|
||||
ALTER TABLE ""Albums"" ALTER COLUMN ""CreatedAt"" TYPE timestamp without time zone
|
||||
USING ""CreatedAt"" AT TIME ZONE 'UTC';");
|
||||
}
|
||||
}
|
||||
}
|
||||
580
Lactose/Migrations/20260710104820_AddModifiedAtToJobRecord.Designer.cs
generated
Normal file
580
Lactose/Migrations/20260710104820_AddModifiedAtToJobRecord.Designer.cs
generated
Normal file
@@ -0,0 +1,580 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260710104820_AddModifiedAtToJobRecord")]
|
||||
partial class AddModifiedAtToJobRecord
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddModifiedAtToJobRecord : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ModifiedAt",
|
||||
table: "JobRecords",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ModifiedAt",
|
||||
table: "JobRecords");
|
||||
}
|
||||
}
|
||||
}
|
||||
577
Lactose/Migrations/20260710164823_RemovePersonDeletedAt.Designer.cs
generated
Normal file
577
Lactose/Migrations/20260710164823_RemovePersonDeletedAt.Designer.cs
generated
Normal file
@@ -0,0 +1,577 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260710164823_RemovePersonDeletedAt")]
|
||||
partial class RemovePersonDeletedAt
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Lactose/Migrations/20260710164823_RemovePersonDeletedAt.cs
Normal file
29
Lactose/Migrations/20260710164823_RemovePersonDeletedAt.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemovePersonDeletedAt : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedAt",
|
||||
table: "People");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "People",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
581
Lactose/Migrations/20260712160403_AddAssetsSearchVisibilityIndex.Designer.cs
generated
Normal file
581
Lactose/Migrations/20260712160403_AddAssetsSearchVisibilityIndex.Designer.cs
generated
Normal file
@@ -0,0 +1,581 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260712160403_AddAssetsSearchVisibilityIndex")]
|
||||
partial class AddAssetsSearchVisibilityIndex
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("IsPubliclyShared", "OwnerId")
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAssetsSearchVisibilityIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets",
|
||||
columns: new[] { "IsPubliclyShared", "OwnerId" },
|
||||
filter: "\"DeletedAt\" IS NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets");
|
||||
}
|
||||
}
|
||||
}
|
||||
594
Lactose/Migrations/20260712162406_AddTrigramIndexes.Designer.cs
generated
Normal file
594
Lactose/Migrations/20260712162406_AddTrigramIndexes.Designer.cs
generated
Normal file
@@ -0,0 +1,594 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260712162406_AddTrigramIndexes")]
|
||||
partial class AddTrigramIndexes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.Property<Guid>("SharedAssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SharedWithId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("SharedAssetsId", "SharedWithId");
|
||||
|
||||
b.HasIndex("SharedWithId");
|
||||
|
||||
b.ToTable("AssetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("Title")
|
||||
.HasDatabaseName("IX_Albums_Title_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Title"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("IsPubliclyShared", "OwnerId")
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_People_Name_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Name"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Name"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetUser", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedAssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SharedWithId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Lactose/Migrations/20260712162406_AddTrigramIndexes.cs
Normal file
50
Lactose/Migrations/20260712162406_AddTrigramIndexes.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrigramIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.Annotation("Npgsql:PostgresExtension:vector", ",,")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:vector", ",,");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_People_Name_Trgm",
|
||||
table: "People",
|
||||
column: "Name")
|
||||
.Annotation("Npgsql:IndexMethod", "gin")
|
||||
.Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Albums_Title_Trgm",
|
||||
table: "Albums",
|
||||
column: "Title")
|
||||
.Annotation("Npgsql:IndexMethod", "gin")
|
||||
.Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_People_Name_Trgm",
|
||||
table: "People");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Albums_Title_Trgm",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:vector", ",,")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:pg_trgm", ",,")
|
||||
.OldAnnotation("Npgsql:PostgresExtension:vector", ",,");
|
||||
}
|
||||
}
|
||||
}
|
||||
591
Lactose/Migrations/20260714154509_AuthRedesign_VisibilityAccessLevel.Designer.cs
generated
Normal file
591
Lactose/Migrations/20260714154509_AuthRedesign_VisibilityAccessLevel.Designer.cs
generated
Normal file
@@ -0,0 +1,591 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20260714154509_AuthRedesign_VisibilityAccessLevel")]
|
||||
partial class AuthRedesign_VisibilityAccessLevel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("Title")
|
||||
.HasDatabaseName("IX_Albums_Title_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Title"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<BitArray>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ThumbnailSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UploadedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UploadedBy");
|
||||
|
||||
b.HasIndex("Visibility", "UploadedBy")
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.JobRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float>("Progress")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_People_Name_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Name"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Name"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.PersonMaintainer", b =>
|
||||
{
|
||||
b.Property<Guid>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("PersonId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PersonMaintainers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Uploader")
|
||||
.WithMany("UploadedAssets")
|
||||
.HasForeignKey("UploadedBy");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.PersonMaintainer", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany()
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Person");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("UploadedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AuthRedesign_VisibilityAccessLevel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Albums_Users_UserOwnerId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Assets_Users_OwnerId",
|
||||
table: "Assets");
|
||||
|
||||
// Remap old EAccessLevel values: old Admin (2) → new Admin (3) first, then old Curator (1) → new Curator (2)
|
||||
migrationBuilder.Sql("UPDATE \"Users\" SET \"AccessLevel\" = 3 WHERE \"AccessLevel\" = 2");
|
||||
migrationBuilder.Sql("UPDATE \"Users\" SET \"AccessLevel\" = 2 WHERE \"AccessLevel\" = 1");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AssetUser");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Albums_UserOwnerId",
|
||||
table: "Albums");
|
||||
|
||||
// Add Visibility columns with default Protected (1) before dropping IsPubliclyShared,
|
||||
// so we can map old values via SQL
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Visibility",
|
||||
table: "People",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Visibility",
|
||||
table: "Assets",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Visibility",
|
||||
table: "Albums",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
|
||||
// Map old IsPubliclyShared = true assets back to Public (0)
|
||||
// Assets that were private default to Protected (1) from the column default
|
||||
migrationBuilder.Sql("UPDATE \"Assets\" SET \"Visibility\" = 0 WHERE \"IsPubliclyShared\" = true");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsPubliclyShared",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserOwnerId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "OwnerId",
|
||||
table: "Assets",
|
||||
newName: "UploadedBy");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Assets_OwnerId",
|
||||
table: "Assets",
|
||||
newName: "IX_Assets_UploadedBy");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PersonMaintainers",
|
||||
columns: table => new
|
||||
{
|
||||
PersonId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PersonMaintainers", x => new { x.PersonId, x.UserId });
|
||||
table.ForeignKey(
|
||||
name: "FK_PersonMaintainers_People_PersonId",
|
||||
column: x => x.PersonId,
|
||||
principalTable: "People",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_PersonMaintainers_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets",
|
||||
columns: new[] { "Visibility", "UploadedBy" },
|
||||
filter: "\"DeletedAt\" IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersonMaintainers_UserId",
|
||||
table: "PersonMaintainers",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Assets_Users_UploadedBy",
|
||||
table: "Assets",
|
||||
column: "UploadedBy",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Assets_Users_UploadedBy",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PersonMaintainers");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Visibility",
|
||||
table: "People");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Visibility",
|
||||
table: "Assets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Visibility",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "UploadedBy",
|
||||
table: "Assets",
|
||||
newName: "OwnerId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Assets_UploadedBy",
|
||||
table: "Assets",
|
||||
newName: "IX_Assets_OwnerId");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsPubliclyShared",
|
||||
table: "Assets",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserOwnerId",
|
||||
table: "Albums",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AssetUser",
|
||||
columns: table => new
|
||||
{
|
||||
SharedAssetsId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SharedWithId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AssetUser", x => new { x.SharedAssetsId, x.SharedWithId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AssetUser_Assets_SharedAssetsId",
|
||||
column: x => x.SharedAssetsId,
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AssetUser_Users_SharedWithId",
|
||||
column: x => x.SharedWithId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assets_VisibleForSearch",
|
||||
table: "Assets",
|
||||
columns: new[] { "IsPubliclyShared", "OwnerId" },
|
||||
filter: "\"DeletedAt\" IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Albums_UserOwnerId",
|
||||
table: "Albums",
|
||||
column: "UserOwnerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AssetUser_SharedWithId",
|
||||
table: "AssetUser",
|
||||
column: "SharedWithId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Albums_Users_UserOwnerId",
|
||||
table: "Albums",
|
||||
column: "UserOwnerId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Assets_Users_OwnerId",
|
||||
table: "Assets",
|
||||
column: "OwnerId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,10 @@ namespace Lactose.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
@@ -60,9 +61,11 @@ namespace Lactose.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
b.Property<Guid?>("CoverAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -72,16 +75,22 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CoverAssetId");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
b.HasIndex("Title")
|
||||
.HasDatabaseName("IX_Albums_Title_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Title"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
@@ -93,10 +102,10 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
@@ -114,9 +123,6 @@ namespace Lactose.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("bit(64)");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
@@ -129,19 +135,27 @@ namespace Lactose.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
b.Property<string>("PreviewFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("PreviewSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(16)");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
@@ -153,7 +167,13 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UploadedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -162,7 +182,11 @@ namespace Lactose.Migrations
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
b.HasIndex("UploadedBy");
|
||||
|
||||
b.HasIndex("Visibility", "UploadedBy")
|
||||
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||
.HasFilter("\"DeletedAt\" IS NULL");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
@@ -221,7 +245,7 @@ namespace Lactose.Migrations
|
||||
|
||||
b.Property<string>("RegexPattern")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -235,10 +259,10 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Finished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("JobType")
|
||||
.HasColumnType("integer");
|
||||
@@ -246,9 +270,12 @@ namespace Lactose.Migrations
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(256)");
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("ParentJobId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -257,7 +284,7 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("Started")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
@@ -274,23 +301,58 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ProfileAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("ProfileCropX")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropY")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<float?>("ProfileCropZoom")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Visibility")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_People_Name_Trgm");
|
||||
|
||||
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Name"), "gin");
|
||||
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Name"), new[] { "gin_trgm_ops" });
|
||||
|
||||
b.HasIndex("ProfileAssetId");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.PersonMaintainer", b =>
|
||||
{
|
||||
b.Property<Guid>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("PersonId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PersonMaintainers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
@@ -304,7 +366,7 @@ namespace Lactose.Migrations
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string[]>("Options")
|
||||
b.PrimitiveCollection<string[]>("Options")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
@@ -348,24 +410,21 @@ namespace Lactose.Migrations
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
@@ -376,10 +435,10 @@ namespace Lactose.Migrations
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
@@ -387,8 +446,6 @@ namespace Lactose.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
@@ -424,17 +481,17 @@ namespace Lactose.Migrations
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("CoverAssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
b.Navigation("CoverAsset");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
@@ -443,13 +500,13 @@ namespace Lactose.Migrations
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
b.HasOne("Lactose.Models.User", "Uploader")
|
||||
.WithMany("UploadedAssets")
|
||||
.HasForeignKey("UploadedBy");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
b.Navigation("Uploader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
@@ -467,6 +524,34 @@ namespace Lactose.Migrations
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "ProfileAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileAssetId");
|
||||
|
||||
b.Navigation("ProfileAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.PersonMaintainer", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany()
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Person");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
@@ -476,18 +561,9 @@ namespace Lactose.Migrations
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany("SharedWith")
|
||||
.HasForeignKey("AssetId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
|
||||
b.Navigation("SharedWith");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
@@ -504,9 +580,7 @@ namespace Lactose.Migrations
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
b.Navigation("UploadedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Butter.Types;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -23,32 +24,32 @@ public class Album {
|
||||
/// Gets or sets the creation date of the album.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime? CreatedAt { get; set; } = DateTime.Now;
|
||||
public DateTime? CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last updated date of the album.
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user owner identifier.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(UserOwner))]
|
||||
public Guid? UserOwnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person owner identifier.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(PersonOwner))]
|
||||
public Guid? PersonOwnerId { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
/// <summary>
|
||||
/// Gets or sets the cover asset identifier.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(CoverAsset))]
|
||||
public Guid? CoverAssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user owner of the album. This is the user account who created/owns the album.
|
||||
/// Gets or sets the visibility level of the album.
|
||||
/// </summary>
|
||||
public User? UserOwner { get; set; }
|
||||
|
||||
public EVisibility Visibility { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person owner of the album. This is the person/model who appears in the album.
|
||||
/// </summary>
|
||||
@@ -59,5 +60,10 @@ public class Album {
|
||||
/// </summary>
|
||||
public List<Asset>? Assets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cover asset for this album.
|
||||
/// </summary>
|
||||
public Asset? CoverAsset { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -43,33 +43,53 @@ public class Asset {
|
||||
/// </summary>
|
||||
public int ThumbnailSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format of the generated thumbnail (e.g. "webp").
|
||||
/// Used to detect when thumbnails need regeneration due to format changes.
|
||||
/// </summary>
|
||||
[Column(TypeName = "VARCHAR(16)")]
|
||||
public string ThumbnailFormat { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the preview path of the asset.
|
||||
/// </summary>
|
||||
[Column(TypeName = "VARCHAR(2048)")]
|
||||
public string PreviewPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the longest-side size in pixels of the generated preview.
|
||||
/// Zero means no preview has been generated.
|
||||
/// </summary>
|
||||
public int PreviewSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format of the generated preview (e.g. "webp").
|
||||
/// Used to detect when previews need regeneration due to format changes.
|
||||
/// </summary>
|
||||
[Column(TypeName = "VARCHAR(16)")]
|
||||
public string PreviewFormat { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the asset.
|
||||
/// </summary>
|
||||
public required EAssetType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owner ID of the asset.
|
||||
/// Gets or sets the ID of the user who uploaded the asset.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Owner))]
|
||||
public Guid? OwnerId { get; set; }
|
||||
[ForeignKey(nameof(UploadedBy))]
|
||||
public Guid? UploadedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the asset is publicly shared.
|
||||
/// Gets or sets the visibility level of the asset.
|
||||
/// </summary>
|
||||
public bool IsPubliclyShared { get; set; }
|
||||
public EVisibility Visibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the creation date of the asset.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
public required DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last updated date of the asset.
|
||||
@@ -116,7 +136,7 @@ public class Asset {
|
||||
/// Computed hash of the asset.
|
||||
/// </summary>
|
||||
[Required][Column(TypeName = "bit(64)")]
|
||||
public BitArray Hash { get; set; }
|
||||
public BitArray Hash { get; set; } = new BitArray(64);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets which folder the asset is in.
|
||||
@@ -127,14 +147,9 @@ public class Asset {
|
||||
#region Navigation Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the owner of the asset.
|
||||
/// Gets or sets the user who uploaded the asset.
|
||||
/// </summary>
|
||||
public User? Owner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of users with whom the asset is shared.
|
||||
/// </summary>
|
||||
public List<User>? SharedWith { get; set; }
|
||||
public User? Uploader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of albums associated with the asset.
|
||||
|
||||
@@ -54,4 +54,9 @@ public class JobRecord {
|
||||
/// Gets or sets the job progress (0 to 1).
|
||||
/// </summary>
|
||||
public float Progress { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp of the last modification to this record.
|
||||
/// Used for delta-based children sync.
|
||||
/// </summary>
|
||||
public DateTime? ModifiedAt { get; set; }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user