Compare commits
480
Commits
4ff3b24df8
...
v1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701c4360b4 | ||
|
|
79252d363d | ||
|
|
8ff21fa56b | ||
|
|
2305e44e68 | ||
|
|
439df56f2b | ||
|
|
f70f256c07 | ||
|
|
3b1f26cdf3 | ||
|
|
9965a13f40 | ||
|
|
fbafa54322 | ||
|
|
9607746912 | ||
|
|
785d33e289 | ||
|
|
a3eeb8b6d1 | ||
|
|
cae61116c8 | ||
|
|
ced0f61b87 | ||
|
|
9751c81574 | ||
|
|
550f195cf2 | ||
|
|
06c6cf7648 | ||
|
|
a5b6642b94 | ||
|
|
f0b67b0404 | ||
|
|
fb2b02e71e | ||
|
|
4658ed1a27 | ||
|
|
52c08089b2 | ||
|
|
111875670b | ||
|
|
9c50f84783 | ||
|
|
593d88d3e3 | ||
|
|
abef50b010 | ||
|
|
ce221428f5 | ||
|
|
213c643263 | ||
|
|
ecc6e02c78 | ||
|
|
b78c5772b4 | ||
|
|
6463df51d1 | ||
|
|
dd2d703fc0 | ||
|
|
a388887f56 | ||
|
|
7d30ea2394 | ||
|
|
ab420fe167 | ||
|
|
19e177504f | ||
|
|
f57e2864c5 | ||
|
|
d89f6f0a21 | ||
|
|
ca217ddb59 | ||
|
|
e025866357 | ||
|
|
1449664678 | ||
|
|
cf8bee555a | ||
|
|
664ac54a7c | ||
|
|
df48dd17f4 | ||
|
|
f15fe8f31a | ||
|
|
8589a85625 | ||
|
|
983cd8baef | ||
|
|
f52b376fca | ||
|
|
75e57353e7 | ||
|
|
eae6152738 | ||
|
|
c4788e1572 | ||
|
|
1dfa9eaf41 | ||
|
|
50b5a28841 | ||
|
|
580cdee67d | ||
|
|
8f0267b0f7 | ||
|
|
5d9aa21987 | ||
|
|
ad1f9e2222 | ||
|
|
90c81c2d20 | ||
|
|
0cc80a084a | ||
|
|
ad7c38b20e | ||
|
|
e0a5bd0ce4 | ||
|
|
e41e7d774f | ||
|
|
34220e93d4 | ||
|
|
22706a09cd | ||
|
|
664c1de5da | ||
|
|
081295af86 | ||
|
|
7dd00c0a5e | ||
|
|
3a195dbc15 | ||
|
|
cce310ee14 | ||
|
|
ada113914d | ||
|
|
a2a25673de | ||
|
|
963e897fe5 | ||
|
|
f9fe91e975 | ||
|
|
01fc121bc3 | ||
|
|
da8a5ad539 | ||
|
|
ff2ed4f7cd | ||
|
|
2c292d2d81 | ||
|
|
f064277c31 | ||
|
|
c5ea749517 | ||
|
|
ebe83fe691 | ||
|
|
ab58ac7dd8 | ||
|
|
17d3f0f2a7 | ||
|
|
1e2bc83b20 | ||
|
|
58014a37bd | ||
|
|
c79ed47e41 | ||
|
|
e48da562fa | ||
|
|
b943d2e595 | ||
|
|
0950106609 | ||
|
|
7ba1ab1551 | ||
|
|
656e6fa9bf | ||
|
|
5e1306e466 | ||
|
|
22565c4f92 | ||
|
|
e62a567e4e | ||
|
|
e5ffe86077 | ||
|
|
d4a53cb2dc | ||
|
|
548b0d5bfd | ||
|
|
92fb5a0149 | ||
|
|
6542dcafd6 | ||
|
|
b7690b5828 | ||
|
|
f3f2bdba02 | ||
|
|
8bf6405c64 | ||
|
|
c1b1feef4d | ||
|
|
0e9820f7c3 | ||
|
|
133c697176 | ||
|
|
8f07d7c445 | ||
|
|
c7e83ed97b | ||
|
|
7db1738678 | ||
|
|
3e099987a3 | ||
|
|
ca50554108 | ||
|
|
2bfa370989 | ||
|
|
dbd5d15fa1 | ||
|
|
b1f32844a9 | ||
|
|
3bf98cbfed | ||
|
|
80c9c8bd6f | ||
|
|
7086fb127f | ||
|
|
19ab8c0975 | ||
|
|
d7f8350ae7 | ||
|
|
f0e3cc9c37 | ||
|
|
4f56af252b | ||
|
|
8aeb4df064 | ||
|
|
dc2983ae55 | ||
|
|
a50dc3aa30 | ||
|
|
3cdad1cebd | ||
|
|
0903da4490 | ||
|
|
2fcd6467e1 | ||
|
|
10337c735c | ||
|
|
e3d4cf9673 | ||
|
|
a203885377 | ||
|
|
fff5ea65df | ||
|
|
86f95d7461 | ||
|
|
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 | ||
|
|
76cabe058b | ||
|
|
33175ffdcb |
+2
-1
@@ -22,4 +22,5 @@
|
|||||||
**/secrets.dev.yaml
|
**/secrets.dev.yaml
|
||||||
**/values.dev.yaml
|
**/values.dev.yaml
|
||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
|
/storageImages
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: Build and Push Containers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version tag (e.g., v1.2.3)'
|
||||||
|
required: true
|
||||||
|
default: 'manual'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
service: [Lactose, MilkStream]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.r3d.codes
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Determine version tag
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
echo "VERSION=$(echo '${{ github.event.inputs.version }}' | sed 's|/|-|g')" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "VERSION=$(echo '${{ gitea.ref_name }}' | sed 's|/|-|g')" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Lowercase repository name
|
||||||
|
run: echo "REPO_LC=$(echo '${{ gitea.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Lowercase service name
|
||||||
|
run: echo "SERVICE_LC=$(echo '${{ matrix.service }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Prepare build args for ${{ matrix.service }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.service }}" = "Lactose" ]; then
|
||||||
|
echo "BUILD_ARGS=SixLaborsLicenseKey=${{ secrets.SIXLABORS_KEY }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "BUILD_ARGS=" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push ${{ matrix.service }}
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./${{ matrix.service }}/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.r3d.codes/${{ env.REPO_LC }}/${{ env.SERVICE_LC }}:${{ env.VERSION }}
|
||||||
|
git.r3d.codes/${{ env.REPO_LC }}/${{ env.SERVICE_LC }}:latest
|
||||||
|
build-args: ${{ env.BUILD_ARGS }}
|
||||||
@@ -7,3 +7,6 @@ riderModule.iml
|
|||||||
storageImages/
|
storageImages/
|
||||||
dotnet-tools.json
|
dotnet-tools.json
|
||||||
.opencode/
|
.opencode/
|
||||||
|
*.css.map
|
||||||
|
# SixLabors license file
|
||||||
|
/Lactose/sixlabors.lic
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
# MilkyShots – Agent Guide
|
# MilkyShots – Agent Guide
|
||||||
|
|
||||||
|
**Gitea repo owner:** MilkyShots
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **.NET 10 SDK** (pinned in `global.json`; `allowPrerelease: true`)
|
- **.NET 10 SDK** (pinned in `global.json`; `allowPrerelease: true`)
|
||||||
- **PostgreSQL with pgvector extension**
|
- **PostgreSQL with pgvector extension**
|
||||||
- **Sass CLI** for SCSS compilation (see SCSS section)
|
- **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`
|
- Run `dotnet tool restore` before first migration — installs `dotnet-ef` (tool manifest at `dotnet-tools.json` in repo root)
|
||||||
|
|
||||||
## Projects (4 in solution)
|
## 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 |
|
| Project | Role | Entrypoint |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Butter** | Shared class library (DTOs, enums, MIME types) | — |
|
| **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` |
|
| **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** | 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
|
## Build & run
|
||||||
|
|
||||||
@@ -30,12 +48,12 @@ dotnet run --project MilkStream # WASM host on :5269 (host) / :8080 (conta
|
|||||||
|
|
||||||
## Infrastructure gotchas
|
## Infrastructure gotchas
|
||||||
|
|
||||||
- **Npgsql legacy timestamps:** `AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true)` is set in `Lactose/Program.cs`. Do not remove without understanding the timestamp implications.
|
|
||||||
- **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.
|
- **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.
|
- **Auth requires claims transformation:** `RefreshTokenTransformation` (registered as `IClaimsTransformation`) must succeed on every authenticated request, or all endpoints return 403.
|
||||||
- **EF Core Tools v8.0.15 vs EF Core Design v10.0.9:** Version mismatch may cause `dotnet ef` CLI quirks.
|
- **`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 (login) and one named `"MilkstreamClient"` with automatic JWT refresh.
|
- **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.
|
- **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
|
## XML docs enforced
|
||||||
|
|
||||||
@@ -44,11 +62,62 @@ dotnet run --project MilkStream # WASM host on :5269 (host) / :8080 (conta
|
|||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **`HttpPut` = create, `HttpPost("{id}")` = update** — not REST-idiomatic; do not "fix" without team buy-in
|
- **`HttpPut` = create, `HttpPost("{id}")` = update** — not REST-idiomatic; do not "fix" without team buy-in
|
||||||
- **Soft delete only** — set `DeletedAt`, never hard-delete (models: Asset, User, Person)
|
- **Soft delete only** — set `DeletedAt`, never hard-delete (models: Asset, User)
|
||||||
- **Repository `Save()` must be called explicitly** after insert/update/delete
|
- **Repository `Save()` must be called explicitly** after insert/update/delete
|
||||||
- **Enum prefix `E`** (e.g. `EAccessLevel`, `EAssetType`, `EJobStatus`)
|
- **Enum prefix `E`** (e.g. `EAccessLevel`, `EAssetType`, `EJobStatus`)
|
||||||
- **JWT access token**: 10 min, refresh token: 60 min (constants in `LactoseAuthService.cs`)
|
- **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`
|
- **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
|
## CORS gotchas
|
||||||
|
|
||||||
@@ -65,25 +134,56 @@ dotnet ef migrations add <Name> --project Lactose
|
|||||||
dotnet ef database update --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
|
## Tests
|
||||||
|
|
||||||
No test project found in solution.
|
No unit/integration test project exists. API endpoint testing is done via **REST Client `.http` files**:
|
||||||
|
|
||||||
## SCSS
|
- **`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.
|
||||||
|
|
||||||
`MilkStream/Styles/MilkyShot.scss` compiles to `MilkStream/wwwroot/css/MilkyShot.css`. The `AspNetCore.SassCompiler` package has been removed (native Dart binary causes Docker crashes), so SCSS **must be compiled manually** when changed:
|
## PR workflow
|
||||||
|
|
||||||
```bash
|
- Before starting work, ask the user whether they want the fix in a PR or directly on `develop` (for quick/trivial fixes).
|
||||||
# Install the Sass CLI once (requires npm):
|
- Larger work always goes in a PR.
|
||||||
npm install -g sass
|
- 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`).
|
||||||
|
|
||||||
# Compile:
|
|
||||||
sass MilkStream/Styles/MilkyShot.scss MilkStream/wwwroot/css/MilkyShot.css
|
|
||||||
```
|
|
||||||
|
|
||||||
Or temporarily add the package, build, then remove it:
|
|
||||||
```bash
|
|
||||||
dotnet add MilkStream/MilkStream.csproj package AspNetCore.SassCompiler
|
|
||||||
dotnet build MilkyShots.sln
|
|
||||||
dotnet remove MilkStream/MilkStream.csproj package AspNetCore.SassCompiler
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Album;
|
namespace Butter.Dtos.Album;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,25 +9,33 @@ public class AlbumAssetPreviewDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the asset ID.
|
/// Gets or sets the asset ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution width.
|
/// Gets or sets the resolution width.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionWidth { get; set; }
|
public int ResolutionWidth { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution height.
|
/// Gets or sets the resolution height.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionHeight { get; set; }
|
public int ResolutionHeight { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset has a thumbnail.
|
/// Gets or sets whether the asset has a thumbnail.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasThumbnail { get; set; }
|
public bool HasThumbnail { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset has a preview.
|
/// Gets or sets whether the asset has a preview.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPreview { get; set; }
|
public bool HasPreview { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the original filename of the asset.
|
/// Gets or sets the original filename of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? FileName { get; set; }
|
public string? FileName { 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 soft-delete timestamp. Only populated for admins and the asset uploader.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Album;
|
namespace Butter.Dtos.Album;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,21 +9,21 @@ public class AlbumCreateDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the album.
|
/// Gets or sets the name of the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owner ID of the album.
|
|
||||||
/// </summary>
|
|
||||||
public Guid? Owner { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person ID associated with the album.
|
/// Gets or sets the person ID associated with the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? Person { get; set; }
|
public Guid? Person { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the cover asset ID for the album.
|
/// Gets or sets the cover asset ID for the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? CoverAssetId { get; set; }
|
public Guid? CoverAssetId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of asset IDs in the album.
|
/// Gets or sets the list of asset IDs in the album.
|
||||||
/// </summary>
|
/// </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;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Butter.Dtos.Album;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to merge multiple albums into a single destination album.
|
||||||
|
/// </summary>
|
||||||
|
public class AlbumMergeDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the destination album that will receive all assets.
|
||||||
|
/// Must not be present in <see cref="SourceIds"/>.
|
||||||
|
/// </summary>
|
||||||
|
public required Guid DestinationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of source album IDs whose assets will be moved to the destination.
|
||||||
|
/// Source albums will be hard-deleted after the merge.
|
||||||
|
/// </summary>
|
||||||
|
public required List<Guid> SourceIds { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Album;
|
namespace Butter.Dtos.Album;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,29 +9,41 @@ public class AlbumPreviewDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the album.
|
/// Gets or sets the unique identifier of the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the album.
|
/// Gets or sets the name of the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owner ID of the album.
|
|
||||||
/// </summary>
|
|
||||||
public Guid? Owner { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person ID associated with the album.
|
/// Gets or sets the person ID associated with the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? Person { get; set; }
|
public Guid? Person { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person name associated with the album.
|
/// Gets or sets the person name associated with the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PersonName { get; set; }
|
public string? PersonName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the ID of the cover asset for this album.
|
/// Gets or sets the ID of the cover asset for this album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? CoverAssetId { get; set; }
|
public Guid? CoverAssetId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the cover image width, if a cover exists.
|
||||||
|
/// </summary>
|
||||||
|
public int? CoverWidth { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the cover image height, if a cover exists.
|
||||||
|
/// </summary>
|
||||||
|
public int? CoverHeight { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of assets in the album.
|
/// Gets or sets the number of assets in the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int AssetCount { get; set; }
|
public int AssetCount { 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 soft-delete timestamp. Always null for albums (not supported).
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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;
|
namespace Butter.Dtos.Album;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,21 +9,25 @@ public class AlbumUpdateDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the album.
|
/// Gets or sets the name of the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Name { get; set; } = string.Empty;
|
public string? Name { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owner ID of the album.
|
|
||||||
/// </summary>
|
|
||||||
public Guid? Owner { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person ID associated with the album.
|
/// Gets or sets the person ID associated with the album.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Gets or sets the cover asset ID for the album.
|
/// Gets or sets the cover asset ID for the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? CoverAssetId { get; set; }
|
public Guid? CoverAssetId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of asset IDs in the album.
|
/// Gets or sets the list of asset IDs in the album.
|
||||||
/// </summary>
|
/// </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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the parameters for browsing assets within a folder at a specific directory level.
|
||||||
|
/// </summary>
|
||||||
|
public class AssetBrowseOptionsDto : PagedSearchParametersDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the root folder ID to browse within.
|
||||||
|
/// </summary>
|
||||||
|
public Guid FolderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current path relative to the folder root (empty for root).
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to only include assets not assigned to any album. Defaults to true.
|
||||||
|
/// </summary>
|
||||||
|
public bool UnlinkedOnly { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of browsing unlinked assets at a specific directory level within a folder.
|
||||||
|
/// </summary>
|
||||||
|
public class AssetBrowseResultDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of immediate subdirectories at the current path level.
|
||||||
|
/// </summary>
|
||||||
|
public List<DirectoryEntryDto> Directories { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of assets located directly at the current path level.
|
||||||
|
/// </summary>
|
||||||
|
public List<AssetPreviewDto> Assets { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current browsing path relative to the folder root.
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentPath { get; set; } = "";
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the total count of assets at the current path level (for pagination).
|
||||||
|
/// </summary>
|
||||||
|
public int TotalAssetCount { get; set; }
|
||||||
|
}
|
||||||
@@ -9,41 +9,42 @@ public class AssetCreateDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the creation date of the asset.
|
/// Gets or sets the creation date of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public Guid Owner { get; set; }
|
public Guid? UploadedBy { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the asset.
|
/// Gets or sets the type of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EAssetType AssetType { get; set; }
|
public EAssetType AssetType { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset is publicly shared.
|
/// Gets or sets the visibility level. Defaults to <see cref="EVisibility.Protected"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPublic { get; set; }
|
public EVisibility Visibility { get; set; } = EVisibility.Private;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the MIME type of the asset.
|
/// Gets or sets the MIME type of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MimeType { get; set; } = string.Empty;
|
public string MimeType { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution width in pixels.
|
/// Gets or sets the resolution width in pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionWidth { get; set; }
|
public int ResolutionWidth { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution height in pixels.
|
/// Gets or sets the resolution height in pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionHeight { get; set; }
|
public int ResolutionHeight { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the file size in bytes.
|
/// Gets or sets the file size in bytes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long FileSize { get; set; }
|
public long FileSize { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the duration in seconds (for video/audio).
|
/// Gets or sets the duration in seconds (for video/audio).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Duration { get; set; }
|
public int? Duration { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the frame rate (for video).
|
/// Gets or sets the frame rate (for video).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? FrameRate { get; set; }
|
public int? FrameRate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,41 +10,33 @@ public class AssetDto : AssetPreviewDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the asset (e.g., Image, Video).
|
/// Gets or sets the type of the asset (e.g., Image, Video).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EAssetType AssetType { get; set; }
|
public EAssetType AssetType { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset is publicly shared.
|
/// Gets or sets the user who uploaded the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPublic { get; set; }
|
public required UserInfoDto UploadedBy { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the owner of the asset.
|
|
||||||
/// </summary>
|
|
||||||
public required UserInfoDto Owner { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the creation date of the asset.
|
/// Gets or sets the creation date of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the last updated date of the asset.
|
/// Gets or sets the last updated date of the asset.
|
||||||
/// </summary>
|
/// </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; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the MIME type of the asset.
|
/// Gets or sets the MIME type of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public new string MimeType { get; set; } = string.Empty;
|
public new string MimeType { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the file size of the asset in bytes.
|
/// Gets or sets the file size of the asset in bytes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long FileSize { get; set; }
|
public long FileSize { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the duration of the asset in seconds (for video/audio).
|
/// Gets or sets the duration of the asset in seconds (for video/audio).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Duration { get; set; }
|
public float Duration { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the frame rate of the asset (for video).
|
/// Gets or sets the frame rate of the asset (for video).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float FrameRate { get; set; }
|
public float FrameRate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a group of unlinked assets (not assigned to any album),
|
||||||
|
/// grouped by folder for drill-down browsing in the asset picker.
|
||||||
|
/// </summary>
|
||||||
|
public class AssetGroupDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the folder ID of the group. Null for assets with no folder or for the maintainer's "My Uploads" group.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? FolderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display name of the group (folder base path, or "My Uploads" for maintainers).
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the count of unlinked assets in this group.
|
||||||
|
/// </summary>
|
||||||
|
public int AssetCount { get; set; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Asset;
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,41 +9,53 @@ public class AssetPreviewDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the asset.
|
/// Gets or sets the unique identifier of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the MIME type of the asset.
|
/// Gets or sets the MIME type of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MimeType { get; set; } = string.Empty;
|
public string MimeType { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution width of the asset in pixels.
|
/// Gets or sets the resolution width of the asset in pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionWidth { get; set; }
|
public int ResolutionWidth { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the resolution height of the asset in pixels.
|
/// Gets or sets the resolution height of the asset in pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ResolutionHeight { get; set; }
|
public int ResolutionHeight { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset has a generated thumbnail.
|
/// Gets or sets whether the asset has a generated thumbnail.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasThumbnail { get; set; }
|
public bool HasThumbnail { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset has a generated preview.
|
/// Gets or sets whether the asset has a generated preview.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPreview { get; set; }
|
public bool HasPreview { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of album titles associated with the asset.
|
/// Gets or sets the list of album titles associated with the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string>? AlbumNames { get; set; }
|
public List<string>? AlbumNames { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of album IDs associated with the asset, parallel to <see cref="AlbumNames"/>.
|
/// Gets or sets the list of album IDs associated with the asset, parallel to <see cref="AlbumNames"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Guid>? AlbumIds { get; set; }
|
public List<Guid>? AlbumIds { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of cosplayer names associated with the asset, derived from album person owners.
|
/// Gets or sets the list of cosplayer names associated with the asset, derived from album person owners.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string>? CosplayerNames { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the original filename of the asset.
|
/// Gets or sets the original filename of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? FileName { get; set; }
|
public string? FileName { 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 soft-delete timestamp. Only populated for admins and the asset uploader.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,16 @@ public class AssetSearchOptionsDto: PagedSearchParametersDto {
|
|||||||
/// Gets or sets whether to order results randomly instead of by date.
|
/// Gets or sets whether to order results randomly instead of by date.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Random { get; set; }
|
public bool Random { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to filter for assets not assigned to any album (unlinked).
|
||||||
|
/// </summary>
|
||||||
|
public bool Unlinked { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the folder ID to filter assets by their scan folder.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? FolderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the uploader user ID to filter assets by their uploader.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? UploadedBy { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Asset;
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -5,15 +7,15 @@ namespace Butter.Dtos.Asset;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AssetUpdateDto {
|
public class AssetUpdateDto {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the new owner ID for the asset.
|
/// Gets or sets the ID of the user who uploaded the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid? Owner { get; set; }
|
public Guid? UploadedBy { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the deletion date to set on the asset.
|
/// Gets or sets the deletion date to set on the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? DeletedAt { get; set; }
|
public DateTime? DeletedAt { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the asset is publicly shared.
|
/// Gets or sets the visibility level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsPublic { get; set; }
|
public EVisibility? Visibility { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Butter.Dtos.Asset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a subdirectory entry in the asset file browser.
|
||||||
|
/// </summary>
|
||||||
|
public class DirectoryEntryDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the directory name (a single path segment, not a full path).
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,12 @@ namespace Butter.Dtos;
|
|||||||
/// Represents pagination parameters for a query.
|
/// Represents pagination parameters for a query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PagedParametersDto {
|
public class PagedParametersDto {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the global constant for max pages.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxPageSize = 250;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the page number (zero-based).
|
/// Gets or sets the page number (zero-based).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -12,6 +18,16 @@ public class PagedParametersDto {
|
|||||||
/// Gets or sets the number of items per page.
|
/// Gets or sets the number of items per page.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PageSize { get; set; } = 150;
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Person;
|
namespace Butter.Dtos.Person;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,5 +9,25 @@ public class PersonCreateDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the person.
|
/// Gets or sets the name of the person.
|
||||||
/// </summary>
|
/// </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;
|
namespace Butter.Dtos.Person;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the full details of a person, including associated assets and albums.
|
/// Represents the full details of a person, including associated albums.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PersonDetailedDto {
|
public class PersonDetailedDto {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the person.
|
/// Gets or sets the unique identifier of the person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Guid Id { get; set; }
|
public required Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the person.
|
/// Gets or sets the name of the person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public List<Guid>? Assets { get; set; }
|
public Guid? ProfileAssetId { get; set; }
|
||||||
/// <summary>
|
/// <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>
|
/// </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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Butter.Dtos.Person;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to merge multiple people into a single destination person.
|
||||||
|
/// </summary>
|
||||||
|
public class PersonMergeDto {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the destination person that will receive all associated data.
|
||||||
|
/// Must not be present in <see cref="SourceIds"/>.
|
||||||
|
/// </summary>
|
||||||
|
public required Guid DestinationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of source person IDs whose albums, faces, and maintainers
|
||||||
|
/// will be reassigned to the destination. Source people will be hard-deleted.
|
||||||
|
/// </summary>
|
||||||
|
public required List<Guid> SourceIds { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Butter.Dtos.Person;
|
namespace Butter.Dtos.Person;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,9 +9,33 @@ public class PersonPreviewDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the person.
|
/// Gets or sets the unique identifier of the person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Guid Id { get; set; }
|
public required Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the person.
|
/// Gets or sets the name of the person.
|
||||||
/// </summary>
|
/// </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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
namespace Butter.Dtos.Person;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,6 +9,29 @@ public class PersonUpdateDto {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the person.
|
/// Gets or sets the name of the person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
//TODO: add the missing properties here
|
/// <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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ public class StatsDto {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int PublicAssets { get; set; }
|
public int PublicAssets { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of non-publicly-shared assets.
|
/// 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>
|
/// </summary>
|
||||||
public int PrivateAssets { get; set; }
|
public int PrivateAssets { get; set; }
|
||||||
|
|
||||||
@@ -116,10 +120,9 @@ public class StatsDto {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int AlbumsMissingCover { get; set; }
|
public int AlbumsMissingCover { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of people/cosplayers without an album that has a cover image.
|
/// Gets or sets the number of people/cosplayers without a profile picture.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Not wired yet — reserved for future use.</remarks>
|
public int CosplayersMissingProfile { get; set; }
|
||||||
public int CosplayersMissingCover { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the top tags by asset count.
|
/// Gets or sets the top tags by asset count.
|
||||||
|
|||||||
@@ -46,4 +46,13 @@ public class UserInfoDto {
|
|||||||
/// Gets or sets the access level of the user.
|
/// Gets or sets the access level of the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required EAccessLevel AccessLevel { get; set; } = EAccessLevel.User;
|
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.
|
/// Gets or sets the new email address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the new password.
|
/// Gets or sets the new password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the user is banned.
|
/// 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>
|
/// </summary>
|
||||||
public bool? IsBanned { get; set; }
|
public bool? IsBanned { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -34,4 +38,8 @@ public class UserUpdateDto {
|
|||||||
/// Gets or sets the new access level.
|
/// Gets or sets the new access level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EAccessLevel? AccessLevel { get; set; }
|
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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ public enum DisplayType {
|
|||||||
TimePicker = 7,
|
TimePicker = 7,
|
||||||
/// <summary>Power of 2 selection.</summary>
|
/// <summary>Power of 2 selection.</summary>
|
||||||
Po2W = 8,
|
Po2W = 8,
|
||||||
|
/// <summary>User picker (searchable dropdown).</summary>
|
||||||
|
UserSelect = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ public enum Settings {
|
|||||||
/// <summary>Quality setting (1–100) for WebP thumbnail encoding.</summary>
|
/// <summary>Quality setting (1–100) for WebP thumbnail encoding.</summary>
|
||||||
ThumbnailQuality,
|
ThumbnailQuality,
|
||||||
/// <summary>Quality setting (1–100) for WebP preview encoding.</summary>
|
/// <summary>Quality setting (1–100) for WebP preview encoding.</summary>
|
||||||
PreviewQuality
|
PreviewQuality,
|
||||||
|
/// <summary>User ID used for scanner-created asset attribution.</summary>
|
||||||
|
SystemUploaderId
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -59,6 +61,7 @@ public static class SettingsExtensions {
|
|||||||
Settings.JobBatchSize => "Job Batch Size",
|
Settings.JobBatchSize => "Job Batch Size",
|
||||||
Settings.ThumbnailQuality => "Thumbnail Quality",
|
Settings.ThumbnailQuality => "Thumbnail Quality",
|
||||||
Settings.PreviewQuality => "Preview Quality",
|
Settings.PreviewQuality => "Preview Quality",
|
||||||
|
Settings.SystemUploaderId => "System Uploader Id",
|
||||||
_ => setting.ToString() // Fallback to the enum name
|
_ => setting.ToString() // Fallback to the enum name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ namespace Butter.Types;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum EAccessLevel {
|
public enum EAccessLevel {
|
||||||
/// <summary>Standard user with basic permissions.</summary>
|
/// <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>
|
/// <summary>Curator with elevated permissions for managing assets.</summary>
|
||||||
Curator,
|
Curator = 2,
|
||||||
/// <summary>Administrator with full system access.</summary>
|
/// <summary>Administrator with full system access.</summary>
|
||||||
Admin
|
Admin = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
namespace Lactose.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for rate-limit buckets applied to different API route groups.
|
||||||
|
/// </summary>
|
||||||
|
public record RateLimitBucketOptions {
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration section name for rate limiting.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "RateLimiting";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default bucket applied to all routes that are not explicitly assigned.
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions Global { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strict bucket for authentication endpoints (login, register, refresh).
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions Auth { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bucket for serving original (full-size) media files.
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions MediaOriginal { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bucket for serving thumbnail media files.
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions MediaThumb { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bucket for serving preview media files.
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions MediaPreview { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bucket for the long-polling jobs endpoint. Uses a shorter window.
|
||||||
|
/// </summary>
|
||||||
|
public BucketOptions Jobs { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings for an individual rate-limit bucket.
|
||||||
|
/// </summary>
|
||||||
|
public class BucketOptions {
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of permits allowed within the window.
|
||||||
|
/// </summary>
|
||||||
|
public int PermitLimit { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Length of the fixed window in seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int WindowSeconds { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of queued requests waiting for a permit. Null disables queueing.
|
||||||
|
/// </summary>
|
||||||
|
public int? QueueLimit { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Lactose.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures <see cref="RateLimitBucketOptions"/> from the application configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class RateLimitBucketOptionsProvider(IConfiguration configuration) : IConfigureOptions<RateLimitBucketOptions> {
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(RateLimitBucketOptions options) {
|
||||||
|
var section = configuration.GetSection(RateLimitBucketOptions.SectionName);
|
||||||
|
if (!section.Exists())
|
||||||
|
return;
|
||||||
|
|
||||||
|
section.Bind(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@ public class LactoseDbContext : DbContext {
|
|||||||
/// Gets or sets the job records DbSet.
|
/// Gets or sets the job records DbSet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<JobRecord> JobRecords { get; set; }
|
public DbSet<JobRecord> JobRecords { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the person-maintainer join table.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<PersonMaintainer> PersonMaintainers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialises a new instance of the <see cref="LactoseDbContext"/> class.
|
/// Initialises a new instance of the <see cref="LactoseDbContext"/> class.
|
||||||
@@ -49,33 +53,62 @@ public class LactoseDbContext : DbContext {
|
|||||||
/// <param name="options">The database context options.</param>
|
/// <param name="options">The database context options.</param>
|
||||||
public LactoseDbContext(DbContextOptions options) : base(options) {}
|
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 />
|
/// <inheritdoc />
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
modelBuilder.HasPostgresExtension("vector");
|
modelBuilder.HasPostgresExtension("vector");
|
||||||
|
modelBuilder.HasPostgresExtension("pg_trgm");
|
||||||
//Album Relationships
|
//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.PersonOwner).WithMany(e => e.Albums);
|
||||||
modelBuilder.Entity<Album>().HasOne(e => e.CoverAsset);
|
modelBuilder.Entity<Album>().HasOne(e => e.CoverAsset);
|
||||||
modelBuilder.Entity<Album>().HasMany(e => e.Assets).WithMany(e => e.Albums);
|
modelBuilder.Entity<Album>().HasMany(e => e.Assets).WithMany(e => e.Albums);
|
||||||
//Asset Relationships
|
//Asset Relationships
|
||||||
modelBuilder.Entity<Asset>().HasOne(e => e.Owner).WithMany(e => e.OwnedAssets);
|
modelBuilder.Entity<Asset>().HasOne(e => e.Uploader).WithMany(e => e.UploadedAssets).HasForeignKey(e => e.UploadedBy);
|
||||||
modelBuilder.Entity<Asset>().HasMany(e => e.SharedWith).WithMany(e => e.SharedAssets);
|
|
||||||
modelBuilder.Entity<Asset>().HasMany(e => e.Albums).WithMany(e => e.Assets);
|
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.Tags).WithMany(e => e.Assets);
|
||||||
modelBuilder.Entity<Asset>().HasMany(e => e.Faces).WithOne(e => e.Asset);
|
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>().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");
|
||||||
|
modelBuilder.Entity<Asset>().HasIndex(a => a.OriginalFilename)
|
||||||
|
.HasDatabaseName("IX_Assets_OriginalFilename_Trgm")
|
||||||
|
.HasMethod("gin")
|
||||||
|
.HasOperators("gin_trgm_ops");
|
||||||
|
// 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
|
//Face Relationships
|
||||||
modelBuilder.Entity<Face>().HasOne(e => e.Asset).WithMany(e => e.Faces);
|
modelBuilder.Entity<Face>().HasOne(e => e.Asset).WithMany(e => e.Faces);
|
||||||
modelBuilder.Entity<Face>().HasOne(e => e.Person).WithMany(e => e.Faces);
|
modelBuilder.Entity<Face>().HasOne(e => e.Person).WithMany(e => e.Faces);
|
||||||
//Person Relationships
|
//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.Faces).WithOne(e => e.Person);
|
||||||
modelBuilder.Entity<Person>().HasMany(e => e.Albums).WithOne(e => e.PersonOwner);
|
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
|
//Tag Relationships
|
||||||
modelBuilder.Entity<Tag>().HasOne(e => e.Parent);
|
modelBuilder.Entity<Tag>().HasOne(e => e.Parent);
|
||||||
modelBuilder.Entity<Tag>().HasMany(e => e.Assets).WithMany(e => e.Tags);
|
modelBuilder.Entity<Tag>().HasMany(e => e.Assets).WithMany(e => e.Tags);
|
||||||
//User Relationships
|
//User Relationships
|
||||||
modelBuilder.Entity<User>().HasMany(e => e.OwnedAssets).WithOne(e => e.Owner);
|
modelBuilder.Entity<User>().HasMany(e => e.UploadedAssets).WithOne(e => e.Uploader).HasForeignKey(e => e.UploadedBy);
|
||||||
modelBuilder.Entity<User>().HasMany(e => e.OwnedAlbums).WithOne(e => e.UserOwner);
|
|
||||||
//Folder Relationships
|
//Folder Relationships
|
||||||
modelBuilder.Entity<Folder>().HasMany(e => e.Assets).WithOne(e => e.Folder);
|
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="authService">Authentication service.</param>
|
||||||
/// <param name="albumRepository">Album repository.</param>
|
/// <param name="albumRepository">Album repository.</param>
|
||||||
/// <param name="assetRepository">Asset repository.</param>
|
/// <param name="assetRepository">Asset repository.</param>
|
||||||
|
/// <param name="personRepository">Person repository.</param>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
@@ -24,7 +25,8 @@ public class AlbumController(
|
|||||||
//ILogger<AlbumController> logger,
|
//ILogger<AlbumController> logger,
|
||||||
LactoseAuthService authService,
|
LactoseAuthService authService,
|
||||||
IAlbumRepository albumRepository,
|
IAlbumRepository albumRepository,
|
||||||
IAssetRepository assetRepository
|
IAssetRepository assetRepository,
|
||||||
|
IPersonRepository personRepository
|
||||||
) : ControllerBase {
|
) : ControllerBase {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an album by its ID with filtered assets based on access level.
|
/// Gets an album by its ID with filtered assets based on access level.
|
||||||
@@ -36,67 +38,39 @@ public class AlbumController(
|
|||||||
Guid? uid = authService.GetUserData(User)?.Id;
|
Guid? uid = authService.GetUserData(User)?.Id;
|
||||||
EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
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 the album does not exist, return a 404
|
||||||
if (album == null) return NotFound();
|
if (album == null) return NotFound();
|
||||||
|
|
||||||
// If the album has no assets, return an empty list
|
return Ok(album.ToAlbumFullDto(accessLevel, uid));
|
||||||
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) && asset.DeletedAt == null));
|
|
||||||
// Add shared assets
|
|
||||||
assets.AddRange(album.Assets.Where(asset => asset.SharedWith!.Any(user => user.Id == uid) && asset.DeletedAt == null));
|
|
||||||
break;
|
|
||||||
case EAccessLevel.Curator:
|
|
||||||
// all assets minus deleted
|
|
||||||
assets.AddRange(album.Assets.Where(asset => asset.DeletedAt == null));
|
|
||||||
break;
|
|
||||||
case EAccessLevel.Admin:
|
|
||||||
// All assets
|
|
||||||
assets.AddRange(album.Assets);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// changes the retrieved album to the new filtered
|
|
||||||
album.Assets = assets.OrderBy(a => a.OriginalFilename).ToList();
|
|
||||||
return Ok(album.ToAlbumFullDto());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches albums with pagination and optional search term.
|
/// Searches albums with pagination, optional search term, sort options, and optional unassigned filter.
|
||||||
/// </summary>
|
/// </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>
|
/// <returns>A list of album previews accessible to the user.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<List<AlbumPreviewDto>> Search([FromQuery] PagedSearchParametersDto pagingOptions) {
|
public ActionResult<List<AlbumPreviewDto>> Search([FromQuery] AlbumSearchParametersDto pagingOptions) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
if(pagingOptions.Page < 0 || pagingOptions.PageSize < 0) return BadRequest();
|
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).ToList();
|
var albums = albumRepository.SearchQuery(
|
||||||
|
pagingOptions.Search ?? string.Empty,
|
||||||
switch (accessLevel) {
|
pagingOptions.Page,
|
||||||
default:
|
pagingOptions.PageSize,
|
||||||
case EAccessLevel.User:
|
pagingOptions.SortBy,
|
||||||
List<Album> filteredAlbums;
|
pagingOptions.SortAsc,
|
||||||
// Only publicly shared albums or owned by user
|
pagingOptions.Unassigned,
|
||||||
filteredAlbums = albums.Where(album => album.Assets != null && (album.UserOwnerId == uid || album.Assets.Any(asset => asset.IsPubliclyShared && asset.DeletedAt == null))).ToList();
|
uid ?? default,
|
||||||
// Add shared albums
|
accessLevel,
|
||||||
filteredAlbums.AddRange(albums.Where(album => album.Assets != null && album.Assets.Any(x => x.DeletedAt == null && x.SharedWith!.Any(user => user.Id == uid))));
|
pagingOptions.AssetUploadedBy
|
||||||
return Ok(filteredAlbums.Select(x => x.ToAlbumPreviewDto()).ToList());
|
).ToList();
|
||||||
case EAccessLevel.Curator:
|
|
||||||
case EAccessLevel.Admin:
|
return Ok(albums);
|
||||||
// All albums
|
|
||||||
return Ok(albums.Select(x => x.ToAlbumPreviewDto()).ToList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -106,7 +80,7 @@ public class AlbumController(
|
|||||||
/// <param name="albumDto">The album update data.</param>
|
/// <param name="albumDto">The album update data.</param>
|
||||||
/// <returns>200 on success, 403 if forbidden, 404 if not found.</returns>
|
/// <returns>200 on success, 403 if forbidden, 404 if not found.</returns>
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public ActionResult Update([FromRoute] Guid id, [FromBody] AlbumUpdateDto albumDto) {
|
public ActionResult Update([FromRoute] Guid id, [FromBody] AlbumUpdateDto albumDto) {
|
||||||
Guid? uid = authService.GetUserData(User)?.Id;
|
Guid? uid = authService.GetUserData(User)?.Id;
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
@@ -116,24 +90,19 @@ public class AlbumController(
|
|||||||
// If the album does not exist, return a 404
|
// If the album does not exist, return a 404
|
||||||
if (album == null) return NotFound();
|
if (album == null) return NotFound();
|
||||||
|
|
||||||
|
// Maintainer scope check — must maintain the album's person
|
||||||
switch (accessLevel) {
|
switch (accessLevel) {
|
||||||
|
case EAccessLevel.Maintainer when album.PersonOwnerId == null || !personRepository.IsMaintainerOf(uid!.Value, album.PersonOwnerId.Value):
|
||||||
|
return Forbid();
|
||||||
case EAccessLevel.User:
|
case EAccessLevel.User:
|
||||||
// Users can't update albums
|
|
||||||
return Forbid();
|
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
|
// Update album properties
|
||||||
album.Title = albumDto.Name ?? album.Title;
|
album.Title = string.IsNullOrEmpty(albumDto.Name) ? album.Title : albumDto.Name;
|
||||||
album.UpdatedAt = DateTime.UtcNow;
|
album.UpdatedAt = DateTime.UtcNow;
|
||||||
album.PersonOwnerId = albumDto.Person ?? album.PersonOwnerId;
|
album.PersonOwnerId = albumDto.RemovePerson ? null : albumDto.Person ?? album.PersonOwnerId;
|
||||||
album.UserOwnerId = albumDto.Owner ?? album.UserOwnerId;
|
album.Visibility = albumDto.Visibility ?? album.Visibility;
|
||||||
album.Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : album.Assets;
|
album.Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : album.Assets;
|
||||||
album.CoverAssetId = albumDto.CoverAssetId ?? album.CoverAssetId;
|
album.CoverAssetId = albumDto.CoverAssetId ?? album.CoverAssetId;
|
||||||
|
|
||||||
@@ -150,7 +119,7 @@ public class AlbumController(
|
|||||||
/// <param name="bulkDto">The bulk update data with album IDs and shared values.</param>
|
/// <param name="bulkDto">The bulk update data with album IDs and shared values.</param>
|
||||||
/// <returns>200 on success, 403 if forbidden.</returns>
|
/// <returns>200 on success, 403 if forbidden.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public ActionResult BulkUpdate([FromBody] BulkDto<AlbumUpdateDto> bulkDto) {
|
public ActionResult BulkUpdate([FromBody] BulkDto<AlbumUpdateDto> bulkDto) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
@@ -161,9 +130,13 @@ public class AlbumController(
|
|||||||
case EAccessLevel.User:
|
case EAccessLevel.User:
|
||||||
// Users can't update albums
|
// Users can't update albums
|
||||||
return Forbid();
|
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:
|
case EAccessLevel.Curator:
|
||||||
// Curators can only update albums they own
|
// Curators can update any album
|
||||||
albums = albums.Where(x => x.UserOwnerId == uid).ToList();
|
|
||||||
break;
|
break;
|
||||||
case EAccessLevel.Admin:
|
case EAccessLevel.Admin:
|
||||||
// Admins can update any album
|
// Admins can update any album
|
||||||
@@ -174,7 +147,7 @@ public class AlbumController(
|
|||||||
album.Title = bulkDto.Data.Name ?? album.Title;
|
album.Title = bulkDto.Data.Name ?? album.Title;
|
||||||
album.UpdatedAt = DateTime.UtcNow;
|
album.UpdatedAt = DateTime.UtcNow;
|
||||||
album.PersonOwnerId = bulkDto.Data.Person ?? album.PersonOwnerId;
|
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.Assets = bulkDto.Data.Assets != null ? assetRepository.FindBulk(bulkDto.Data.Assets).ToList() : album.Assets;
|
||||||
album.CoverAssetId = bulkDto.Data.CoverAssetId ?? album.CoverAssetId;
|
album.CoverAssetId = bulkDto.Data.CoverAssetId ?? album.CoverAssetId;
|
||||||
}
|
}
|
||||||
@@ -191,14 +164,10 @@ public class AlbumController(
|
|||||||
/// <param name="albumDto">The album creation data.</param>
|
/// <param name="albumDto">The album creation data.</param>
|
||||||
/// <returns>200 on success.</returns>
|
/// <returns>200 on success.</returns>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Curator,Admin")]
|
||||||
public ActionResult Create([FromBody] AlbumCreateDto albumDto) {
|
public ActionResult Create([FromBody] AlbumCreateDto albumDto) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
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 assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : new List<Asset>();
|
||||||
|
|
||||||
var album = new Album {
|
var album = new Album {
|
||||||
@@ -206,7 +175,7 @@ public class AlbumController(
|
|||||||
Title = albumDto.Name,
|
Title = albumDto.Name,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
PersonOwnerId = albumDto.Person ?? null,
|
PersonOwnerId = albumDto.Person ?? null,
|
||||||
UserOwnerId = albumDto.Owner,
|
Visibility = albumDto.Visibility,
|
||||||
CoverAssetId = albumDto.CoverAssetId ?? assets.OrderBy(a => a.OriginalFilename).FirstOrDefault()?.Id,
|
CoverAssetId = albumDto.CoverAssetId ?? assets.OrderBy(a => a.OriginalFilename).FirstOrDefault()?.Id,
|
||||||
Assets = assets
|
Assets = assets
|
||||||
};
|
};
|
||||||
@@ -214,6 +183,26 @@ public class AlbumController(
|
|||||||
albumRepository.Insert(album);
|
albumRepository.Insert(album);
|
||||||
albumRepository.Save();
|
albumRepository.Save();
|
||||||
|
|
||||||
|
return Ok(album.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges multiple source albums into a destination album.
|
||||||
|
/// All assets from source albums are moved to the destination, then sources are hard-deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The merge request with destination and source IDs.</param>
|
||||||
|
/// <returns>200 on success, 400 if destination is in sources or not found.</returns>
|
||||||
|
[HttpPost("merge")]
|
||||||
|
[Authorize(Roles = "Curator,Admin")]
|
||||||
|
public ActionResult Merge([FromBody] AlbumMergeDto dto) {
|
||||||
|
if (dto.SourceIds.Contains(dto.DestinationId))
|
||||||
|
return BadRequest("Destination album ID must not appear in source album IDs.");
|
||||||
|
|
||||||
|
if (albumRepository.Find(dto.DestinationId) == null)
|
||||||
|
return NotFound("Destination album not found.");
|
||||||
|
|
||||||
|
albumRepository.MergeAlbums(dto.DestinationId, dto.SourceIds);
|
||||||
|
albumRepository.Save();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,18 +212,23 @@ public class AlbumController(
|
|||||||
/// <param name="albumIds">The list of album IDs to remove.</param>
|
/// <param name="albumIds">The list of album IDs to remove.</param>
|
||||||
/// <returns>200 on success, 403 if forbidden.</returns>
|
/// <returns>200 on success, 403 if forbidden.</returns>
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public ActionResult BulkDelete([FromBody] List<Guid> albumIds) {
|
public ActionResult BulkDelete([FromBody] List<Guid> albumIds) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
var albums = albumRepository.FindBulk(albumIds).ToList();
|
var albums = albumRepository.FindBulk(albumIds).ToList();
|
||||||
|
|
||||||
// If the user is not an admin, filter out albums they do not own
|
// Filter by access level
|
||||||
if (accessLevel != EAccessLevel.Admin) { albums = albums.Where(x => x.UserOwnerId == uid).ToList(); }
|
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 (albums.Count == 0) return Forbid();
|
||||||
if (albumIds.Count == 0) return Forbid();
|
|
||||||
|
|
||||||
albums.ForEach(albumRepository.Remove);
|
albums.ForEach(albumRepository.Remove);
|
||||||
albumRepository.Save();
|
albumRepository.Save();
|
||||||
@@ -248,7 +242,7 @@ public class AlbumController(
|
|||||||
/// <param name="id">The album ID.</param>
|
/// <param name="id">The album ID.</param>
|
||||||
/// <returns>200 on success, 404 if not found, 403 if forbidden.</returns>
|
/// <returns>200 on success, 404 if not found, 403 if forbidden.</returns>
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public ActionResult Delete(Guid id) {
|
public ActionResult Delete(Guid id) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
@@ -257,8 +251,13 @@ public class AlbumController(
|
|||||||
|
|
||||||
// If the album does not exist, return a 404
|
// If the album does not exist, return a 404
|
||||||
if (album == null) return NotFound();
|
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.Remove(album);
|
||||||
albumRepository.Save();
|
albumRepository.Save();
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ namespace Lactose.Controllers;
|
|||||||
/// <param name="authService">Authentication service.</param>
|
/// <param name="authService">Authentication service.</param>
|
||||||
/// <param name="assetRepository">Asset repository.</param>
|
/// <param name="assetRepository">Asset repository.</param>
|
||||||
/// <param name="userRepository">User repository.</param>
|
/// <param name="userRepository">User repository.</param>
|
||||||
|
/// <param name="personRepository">Person repository (for maintainer scope checks).</param>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class AssetController(
|
public class AssetController(
|
||||||
ILogger<AssetController> logger,
|
ILogger<AssetController> logger,
|
||||||
LactoseAuthService authService,
|
LactoseAuthService authService,
|
||||||
IAssetRepository assetRepository,
|
IAssetRepository assetRepository,
|
||||||
IUserRepository userRepository
|
IUserRepository userRepository,
|
||||||
|
IPersonRepository personRepository
|
||||||
) : ControllerBase {
|
) : ControllerBase {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an asset by its ID with full details, respecting access level.
|
/// 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) {
|
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();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (accessLevel) {
|
AssetDto dto = asset.ToFullAssetsDto(accessLevel, uid);
|
||||||
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);
|
|
||||||
|
|
||||||
logger.LogTrace(
|
logger.LogTrace(
|
||||||
$"""
|
$"""
|
||||||
@@ -95,11 +61,11 @@ public class AssetController(
|
|||||||
dto.Id: {dto.Id}
|
dto.Id: {dto.Id}
|
||||||
dto.FileName: {dto.FileName}
|
dto.FileName: {dto.FileName}
|
||||||
dto.AssetType: {dto.AssetType}
|
dto.AssetType: {dto.AssetType}
|
||||||
dto.IsPublic: {dto.IsPublic}
|
dto.Visibility: {dto.Visibility}
|
||||||
dto.CreatedAt: {dto.CreatedAt}
|
dto.CreatedAt: {dto.CreatedAt}
|
||||||
dto.UpdatedAt: {dto.UpdatedAt}
|
dto.UpdatedAt: {dto.UpdatedAt}
|
||||||
dto.DeletedAt: {dto.DeletedAt}
|
dto.DeletedAt: {dto.DeletedAt}
|
||||||
dto.Owner: {dto.Owner.Id} {dto.Owner.Username}
|
dto.UploadedBy: {dto.UploadedBy.Id} {dto.UploadedBy.Username}
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,9 +73,9 @@ public class AssetController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches assets with optional date range, type filter, and random ordering with pagination.
|
/// Searches assets with optional date range, type filter, random ordering, unlinked filter, folder filter, uploader filter, search term, and pagination.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchOptionsDto">Search options including date range, type filter, random ordering, and pagination.</param>
|
/// <param name="searchOptionsDto">Search options including date range, type filter, unlinked filter, folder filter, uploader filter, random ordering, search term, and pagination.</param>
|
||||||
/// <returns>A list of asset previews.</returns>
|
/// <returns>A list of asset previews.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<List<AssetPreviewDto>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto) {
|
public ActionResult<List<AssetPreviewDto>> GetAll([FromQuery] AssetSearchOptionsDto searchOptionsDto) {
|
||||||
@@ -124,14 +90,16 @@ public class AssetController(
|
|||||||
return BadRequest();
|
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");
|
logger.LogWarning("Invalid pagination parameters provided");
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
var assets = assetRepository.GetAssets(
|
var assets = assetRepository.GetAssets(
|
||||||
searchOptionsDto.Type, from, to, searchOptionsDto.Random, searchOptionsDto.Seed,
|
searchOptionsDto.Type, from, to, searchOptionsDto.Random, searchOptionsDto.Seed,
|
||||||
searchOptionsDto.Page, searchOptionsDto.PageSize, out int total
|
searchOptionsDto.Page, searchOptionsDto.PageSize, out int total,
|
||||||
|
uid, accessLevel,
|
||||||
|
searchOptionsDto.Unlinked, searchOptionsDto.FolderId, searchOptionsDto.UploadedBy, searchOptionsDto.Search
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.LogTrace(
|
logger.LogTrace(
|
||||||
@@ -146,27 +114,56 @@ public class AssetController(
|
|||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (accessLevel) {
|
var dtoList = assets.ToAssetPreviewDto(accessLevel, uid).ToList();
|
||||||
case EAccessLevel.User: {
|
|
||||||
assets = assets.Where(
|
|
||||||
a => a.DeletedAt == null && (a.IsPubliclyShared || a.SharedWith?.Find(x => x.Id == uid) != null)
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EAccessLevel.Curator:
|
|
||||||
case EAccessLevel.Admin: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dtoList = assets.ToAssetPreviewDto().ToList();
|
|
||||||
|
|
||||||
logger.LogTrace($"Returning {dtoList.Count} assets (total: {total})");
|
logger.LogTrace($"Returning {dtoList.Count} assets (total: {total})");
|
||||||
Response.Headers["X-Total-Count"] = total.ToString();
|
Response.Headers["X-Total-Count"] = total.ToString();
|
||||||
return Ok(dtoList);
|
return Ok(dtoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns groups of unlinked assets (not assigned to any album), grouped by folder for drill-down browsing.
|
||||||
|
/// Admins and curators receive all folders with their unlinked asset counts. Maintainers receive a single
|
||||||
|
/// "My Uploads" group with their own unlinked upload count.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of unlinked asset groups with folder ID, name, and asset count.</returns>
|
||||||
|
[HttpGet("unlinked-groups")]
|
||||||
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
|
public ActionResult<List<AssetGroupDto>> GetFolderGroups([FromQuery] bool unlinkedOnly = true) {
|
||||||
|
var userData = authService.GetUserData(User);
|
||||||
|
var uid = userData?.Id;
|
||||||
|
EAccessLevel accessLevel = userData?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
logger.LogTrace("GetFolderGroups: uid={Uid}, accessLevel={AccessLevel}, unlinkedOnly={UnlinkedOnly}", uid, accessLevel, unlinkedOnly);
|
||||||
|
|
||||||
|
var groups = assetRepository.GetFolderGroups(uid, accessLevel, unlinkedOnly);
|
||||||
|
logger.LogTrace("GetFolderGroups: returned {Count} groups", groups.Count);
|
||||||
|
return Ok(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Browses assets at a specific directory level within a folder, returning immediate subdirectories
|
||||||
|
/// and a paginated list of assets at that level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Browse options including folder ID, path, pagination, search, and unlinked-only filter.</param>
|
||||||
|
/// <returns>A browse result with subdirectories and assets at the current level.</returns>
|
||||||
|
[HttpGet("browse")]
|
||||||
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
|
public ActionResult<AssetBrowseResultDto> Browse([FromQuery] AssetBrowseOptionsDto options) {
|
||||||
|
var userData = authService.GetUserData(User);
|
||||||
|
var uid = userData?.Id;
|
||||||
|
EAccessLevel accessLevel = userData?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
if (options.Page < 0 || options.PageSize < 1 || options.PageSize > PagedParametersDto.MaxPageSize)
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
logger.LogTrace("Browse: folderId={FolderId}, path={Path}, page={Page}, accessLevel={AccessLevel}, unlinkedOnly={UnlinkedOnly}", options.FolderId, options.Path, options.Page, accessLevel, options.UnlinkedOnly);
|
||||||
|
|
||||||
|
var result = assetRepository.BrowseAssets(options.FolderId, options.Path ?? "", options.Page, options.PageSize, uid, accessLevel, options.Search, options.UnlinkedOnly);
|
||||||
|
if (result == null) return NotFound();
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an asset's metadata.
|
/// Updates an asset's metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -174,32 +171,35 @@ public class AssetController(
|
|||||||
/// <param name="dto">The asset update data.</param>
|
/// <param name="dto">The asset update data.</param>
|
||||||
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
[Authorize(Roles = "Curator,Admin")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] AssetUpdateDto dto) {
|
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] AssetUpdateDto dto) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||||
var asset = assetRepository.Find(id);
|
var asset = assetRepository.FindWithAlbums(id);
|
||||||
|
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||||
return NotFound();
|
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");
|
logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to update asset {id} that they do not own");
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
string log = "";
|
string log = "";
|
||||||
|
|
||||||
asset.IsPubliclyShared = dto.IsPublic ?? asset.IsPubliclyShared;
|
asset.Visibility = dto.Visibility ?? asset.Visibility;
|
||||||
log += $"PubliclyShared: {asset.IsPubliclyShared} -> {dto.IsPublic ?? asset.IsPubliclyShared}\n";
|
log += $"Visibility: {asset.Visibility} -> {dto.Visibility ?? asset.Visibility}\n";
|
||||||
|
|
||||||
asset.DeletedAt = dto.DeletedAt ?? asset.DeletedAt;
|
asset.DeletedAt = dto.DeletedAt ?? asset.DeletedAt;
|
||||||
log += $"DeletedAt: {asset.DeletedAt} -> {dto.DeletedAt ?? asset.DeletedAt}\n";
|
log += $"DeletedAt: {asset.DeletedAt} -> {dto.DeletedAt ?? asset.DeletedAt}\n";
|
||||||
|
|
||||||
asset.Owner = dto.Owner != null ? userRepository.Find(dto.Owner.Value) : asset.Owner;
|
if (accesslevel == EAccessLevel.Admin) {
|
||||||
log += $"Owner: {asset.Owner?.Id} -> {dto.Owner ?? asset.Owner?.Id}\n";
|
asset.UploadedBy = dto.UploadedBy ?? asset.UploadedBy;
|
||||||
|
log += $"UploadedBy: {asset.UploadedBy} -> {dto.UploadedBy ?? asset.UploadedBy}\n";
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogTrace($"Updating asset {id}\n{log}");
|
logger.LogTrace($"Updating asset {id}\n{log}");
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ public class AssetController(
|
|||||||
/// <param name="bulkDto">The bulk update data with asset IDs and shared values.</param>
|
/// <param name="bulkDto">The bulk update data with asset IDs and shared values.</param>
|
||||||
/// <returns>200 on success, or an error response.</returns>
|
/// <returns>200 on success, or an error response.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Curator,Admin")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<AssetUpdateDto> bulkDto) {
|
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<AssetUpdateDto> bulkDto) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||||
@@ -234,17 +234,24 @@ public class AssetController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//If the user is not an admin, they can only update their own assets
|
//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(
|
enumerable.ForEach(
|
||||||
x => {
|
x => {
|
||||||
x.IsPubliclyShared = bulkDto.Data.IsPublic ?? x.IsPubliclyShared;
|
x.Visibility = bulkDto.Data.Visibility ?? x.Visibility;
|
||||||
x.DeletedAt = bulkDto.Data.DeletedAt ?? x.DeletedAt;
|
x.DeletedAt = bulkDto.Data.DeletedAt ?? x.DeletedAt;
|
||||||
x.Owner = bulkDto.Data.Owner != null ? userRepository.Find(bulkDto.Data.Owner.Value) : x.Owner;
|
|
||||||
|
if (accesslevel == EAccessLevel.Admin) {
|
||||||
|
x.UploadedBy = bulkDto.Data.UploadedBy ?? x.UploadedBy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assetRepository.UpdateBulk(enumerable);
|
assetRepository.UpdateBulk(enumerable);
|
||||||
|
assetRepository.Save();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,18 +261,19 @@ public class AssetController(
|
|||||||
/// <param name="id">The asset ID.</param>
|
/// <param name="id">The asset ID.</param>
|
||||||
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
/// <returns>200 on success, 404 if not found, 401 if unauthorized.</returns>
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[Authorize(Roles = "Curator,Admin")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public IStatusCodeActionResult Delete([FromRoute] Guid id) {
|
public IStatusCodeActionResult Delete([FromRoute] Guid id) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||||
var asset = assetRepository.Find(id);
|
var asset = assetRepository.FindWithAlbums(id);
|
||||||
|
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
logger.LogWarning($"Request asset {id} not found in the database!");
|
logger.LogWarning($"Request asset {id} not found in the database!");
|
||||||
return NotFound();
|
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");
|
logger.LogWarning($"User {authService.GetUserData(User)?.Id} is trying to delete asset {id} that they do not own");
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
@@ -283,7 +291,7 @@ public class AssetController(
|
|||||||
/// <param name="ids">The list of asset IDs to delete.</param>
|
/// <param name="ids">The list of asset IDs to delete.</param>
|
||||||
/// <returns>200 on success, or an error response.</returns>
|
/// <returns>200 on success, or an error response.</returns>
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Authorize(Roles = "Curator,Admin")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public IStatusCodeActionResult BulkDelete([FromBody] List<Guid> ids) {
|
public IStatusCodeActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||||
@@ -302,11 +310,15 @@ public class AssetController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//If the user is not an admin, they can only delete their own assets
|
//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.UtcNow);
|
enumerable.ForEach(x => x.DeletedAt = DateTime.UtcNow);
|
||||||
|
|
||||||
assetRepository.UpdateBulk(enumerable);
|
assetRepository.UpdateBulk(enumerable);
|
||||||
|
assetRepository.Save();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,23 +334,22 @@ public class AssetController(
|
|||||||
var uid = authService.GetUserData(User)?.Id;
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
var accesslevel = authService.GetUserData(User)!.AccessLevel;
|
||||||
|
|
||||||
if (userRepository.Find(dto.Owner) == null) {
|
if (dto.UploadedBy.HasValue) {
|
||||||
logger.LogWarning($"User {dto.Owner} does not exist in the database");
|
if (userRepository.Find(dto.UploadedBy.Value) == null) {
|
||||||
return BadRequest();
|
logger.LogWarning($"User {dto.UploadedBy} does not exist in the database");
|
||||||
}
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
if (accesslevel != EAccessLevel.Admin && dto.Owner != uid) {
|
if (accesslevel != EAccessLevel.Admin && dto.UploadedBy != uid) {
|
||||||
logger.LogWarning($"User {uid} is trying to create an asset for user {dto.Owner}");
|
logger.LogWarning($"User {uid} is trying to create an asset for user {dto.UploadedBy}");
|
||||||
return Unauthorized();
|
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 {
|
var asset = new Asset {
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Type = dto.AssetType,
|
Type = dto.AssetType,
|
||||||
IsPubliclyShared = dto.IsPublic,
|
Visibility = dto.Visibility,
|
||||||
OwnerId = dto.Owner,
|
UploadedBy = dto.UploadedBy,
|
||||||
CreatedAt = dto.CreatedAt,
|
CreatedAt = dto.CreatedAt,
|
||||||
MimeType = dto.MimeType,
|
MimeType = dto.MimeType,
|
||||||
ResolutionWidth = dto.ResolutionWidth,
|
ResolutionWidth = dto.ResolutionWidth,
|
||||||
@@ -355,4 +366,14 @@ public class AssetController(
|
|||||||
|
|
||||||
return Ok(asset.Id);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Butter.Dtos;
|
using Butter.Dtos;
|
||||||
using Butter.Dtos.User;
|
using Butter.Dtos.User;
|
||||||
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
@@ -7,6 +8,7 @@ using Lactose.Services;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
namespace Lactose.Controllers;
|
namespace Lactose.Controllers;
|
||||||
|
|
||||||
@@ -17,13 +19,16 @@ namespace Lactose.Controllers;
|
|||||||
/// <param name="authService">Authentication service.</param>
|
/// <param name="authService">Authentication service.</param>
|
||||||
/// <param name="userRepository">User repository.</param>
|
/// <param name="userRepository">User repository.</param>
|
||||||
/// <param name="passwordHasher">Password hasher.</param>
|
/// <param name="passwordHasher">Password hasher.</param>
|
||||||
|
/// <param name="settingsRepository">Settings repository.</param>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
|
[EnableRateLimiting("auth")]
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
ILogger<AuthController> logger,
|
ILogger<AuthController> logger,
|
||||||
LactoseAuthService authService,
|
LactoseAuthService authService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPasswordHasher<User> passwordHasher
|
IPasswordHasher<User> passwordHasher,
|
||||||
|
ISettingsRepository settingsRepository
|
||||||
) : ControllerBase {
|
) : ControllerBase {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -92,14 +97,28 @@ public class AuthController(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether user registration is currently enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>200 if enabled, 403 if disabled.</returns>
|
||||||
|
[HttpGet("register")]
|
||||||
|
public ActionResult CheckRegistrationEnabled() {
|
||||||
|
var regSetting = settingsRepository.Get(Settings.UserRegistrationEnabled.AsString());
|
||||||
|
return regSetting?.Value == "true" ? Ok() : StatusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a new user account.
|
/// Registers a new user account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The registration data.</param>
|
/// <param name="dto">The registration data.</param>
|
||||||
/// <returns>200 with the new user ID, or 409 if the user already exists.</returns>
|
/// <returns>200 with the new user ID, 403 if registration is disabled, or 409 if the user already exists.</returns>
|
||||||
//TODO: Switch Guid reply with Authentication Result (giving a complete reason in case of failure or giving the authentication token)
|
//TODO: Switch Guid reply with Authentication Result (giving a complete reason in case of failure or giving the authentication token)
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public ActionResult Register([FromBody] UserRegisterDto dto) {
|
public ActionResult Register([FromBody] UserRegisterDto dto) {
|
||||||
|
var regSetting = settingsRepository.Get(Settings.UserRegistrationEnabled.AsString());
|
||||||
|
if (regSetting?.Value != "true")
|
||||||
|
return StatusCode(403, "Registration is currently disabled on this server");
|
||||||
|
|
||||||
logger.LogDebug(
|
logger.LogDebug(
|
||||||
$"""
|
$"""
|
||||||
Registering user:
|
Registering user:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
using Butter.Dtos;
|
||||||
using Butter.Dtos.Jobs;
|
using Butter.Dtos.Jobs;
|
||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Jobs;
|
using Lactose.Jobs;
|
||||||
using Lactose.Mapper;
|
|
||||||
using Lactose.Services;
|
using Lactose.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
namespace Lactose.Controllers;
|
namespace Lactose.Controllers;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ namespace Lactose.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
|
[EnableRateLimiting("jobs")]
|
||||||
public class JobsController(JobManager jobManager, JobScheduler jobScheduler, LactoseAuthService authService) : ControllerBase {
|
public class JobsController(JobManager jobManager, JobScheduler jobScheduler, LactoseAuthService authService) : ControllerBase {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,7 +37,8 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("past")]
|
[HttpGet("past")]
|
||||||
[Authorize(Roles = "Admin")]
|
[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;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||||
var (jobs, total) = jobManager.GetPastRootJobs(page, pageSize);
|
var (jobs, total) = jobManager.GetPastRootJobs(page, pageSize);
|
||||||
@@ -44,13 +47,16 @@ public class JobsController(JobManager jobManager, JobScheduler jobScheduler, La
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns children of a specific parent job (active + past).
|
/// 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>
|
/// </summary>
|
||||||
[HttpGet("{parentId}/children")]
|
[HttpGet("{parentId}/children")]
|
||||||
[Authorize(Roles = "Admin")]
|
[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;
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Lactose.Repositories;
|
|||||||
using Lactose.Services;
|
using Lactose.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
@@ -15,6 +16,7 @@ namespace Lactose.Controllers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authService">Authentication service.</param>
|
/// <param name="authService">Authentication service.</param>
|
||||||
/// <param name="mediaRepository">Media repository.</param>
|
/// <param name="mediaRepository">Media repository.</param>
|
||||||
|
/// <param name="assetRepository">Asset repository.</param>
|
||||||
/// <param name="signKeyOptions">JWT signing key configuration.</param>
|
/// <param name="signKeyOptions">JWT signing key configuration.</param>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -34,6 +36,7 @@ public class MediaController(
|
|||||||
/// <returns>The physical image file, or 404 if not found or access denied.</returns>
|
/// <returns>The physical image file, or 404 if not found or access denied.</returns>
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[HttpGet("original/{id}")]
|
[HttpGet("original/{id}")]
|
||||||
|
[EnableRateLimiting("media_original")]
|
||||||
public ActionResult GetImage(Guid id) {
|
public ActionResult GetImage(Guid id) {
|
||||||
var user = GetUserFromRequest();
|
var user = GetUserFromRequest();
|
||||||
var asset = assetRepository.Find(id);
|
var asset = assetRepository.Find(id);
|
||||||
@@ -43,7 +46,7 @@ public class MediaController(
|
|||||||
if (CanAccessAssetDirectly(user, asset.DeletedAt)) {
|
if (CanAccessAssetDirectly(user, asset.DeletedAt)) {
|
||||||
return PhysicalFile(asset.OriginalPath, asset.MimeType);
|
return PhysicalFile(asset.OriginalPath, asset.MimeType);
|
||||||
}
|
}
|
||||||
var data = mediaRepository.GetOriginalData(id, user?.Id);
|
var data = mediaRepository.GetOriginalData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||||
if (data == null) return NotFound();
|
if (data == null) return NotFound();
|
||||||
|
|
||||||
return PhysicalFile(data.Path, data.MimeType);
|
return PhysicalFile(data.Path, data.MimeType);
|
||||||
@@ -55,6 +58,7 @@ public class MediaController(
|
|||||||
/// <param name="id">The asset ID.</param>
|
/// <param name="id">The asset ID.</param>
|
||||||
/// <returns>The physical thumbnail file, or 404 if not found or access denied.</returns>
|
/// <returns>The physical thumbnail file, or 404 if not found or access denied.</returns>
|
||||||
[HttpGet("thumb/{id}")]
|
[HttpGet("thumb/{id}")]
|
||||||
|
[EnableRateLimiting("media_thumb")]
|
||||||
public ActionResult GetThumb(Guid id) {
|
public ActionResult GetThumb(Guid id) {
|
||||||
var user = GetUserFromRequest();
|
var user = GetUserFromRequest();
|
||||||
var asset = assetRepository.Find(id);
|
var asset = assetRepository.Find(id);
|
||||||
@@ -65,7 +69,7 @@ public class MediaController(
|
|||||||
if (string.IsNullOrEmpty(asset.ThumbnailPath)) return NotFound();
|
if (string.IsNullOrEmpty(asset.ThumbnailPath)) return NotFound();
|
||||||
return PhysicalFile(asset.ThumbnailPath, "image/webp");
|
return PhysicalFile(asset.ThumbnailPath, "image/webp");
|
||||||
}
|
}
|
||||||
var data = mediaRepository.GetThumbData(id, user?.Id);
|
var data = mediaRepository.GetThumbData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||||
if (data == null) return NotFound();
|
if (data == null) return NotFound();
|
||||||
|
|
||||||
return PhysicalFile(data.Path, data.MimeType);
|
return PhysicalFile(data.Path, data.MimeType);
|
||||||
@@ -77,6 +81,7 @@ public class MediaController(
|
|||||||
/// <param name="id">The asset ID.</param>
|
/// <param name="id">The asset ID.</param>
|
||||||
/// <returns>The physical preview file, or 404 if not found or access denied.</returns>
|
/// <returns>The physical preview file, or 404 if not found or access denied.</returns>
|
||||||
[HttpGet("preview/{id}")]
|
[HttpGet("preview/{id}")]
|
||||||
|
[EnableRateLimiting("media_preview")]
|
||||||
public ActionResult GetPreview(Guid id) {
|
public ActionResult GetPreview(Guid id) {
|
||||||
var user = GetUserFromRequest();
|
var user = GetUserFromRequest();
|
||||||
var asset = assetRepository.Find(id);
|
var asset = assetRepository.Find(id);
|
||||||
@@ -87,7 +92,7 @@ public class MediaController(
|
|||||||
if (string.IsNullOrEmpty(asset.PreviewPath)) return NotFound();
|
if (string.IsNullOrEmpty(asset.PreviewPath)) return NotFound();
|
||||||
return PhysicalFile(asset.PreviewPath, "image/webp");
|
return PhysicalFile(asset.PreviewPath, "image/webp");
|
||||||
}
|
}
|
||||||
var data = mediaRepository.GetPreviewData(id, user?.Id);
|
var data = mediaRepository.GetPreviewData(id, user?.Id, user?.AccessLevel ?? EAccessLevel.User);
|
||||||
if (data == null) return NotFound();
|
if (data == null) return NotFound();
|
||||||
|
|
||||||
return PhysicalFile(data.Path, data.MimeType);
|
return PhysicalFile(data.Path, data.MimeType);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using Butter.Dtos;
|
using Butter.Dtos;
|
||||||
using Butter.Dtos.Person;
|
using Butter.Dtos.Person;
|
||||||
|
using Butter.Types;
|
||||||
|
using Lactose.Mapper;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
|
using Lactose.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
||||||
|
|
||||||
namespace Lactose.Controllers;
|
namespace Lactose.Controllers;
|
||||||
|
|
||||||
@@ -14,31 +16,56 @@ namespace Lactose.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class PersonController(
|
public class PersonController(
|
||||||
IPersonRepository personRepository
|
IPersonRepository personRepository,
|
||||||
|
IAlbumRepository albumRepository,
|
||||||
|
LactoseAuthService authService
|
||||||
) : ControllerBase {
|
) : ControllerBase {
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="id">The person ID.</param>
|
/// <param name="id">The person ID.</param>
|
||||||
/// <param name="assets">Maximum number of associated assets to include.</param>
|
/// <param name="albumPaging">Pagination, search, and sort parameters for albums.</param>
|
||||||
/// <param name="albums">Maximum number of associated albums to include.</param>
|
/// <returns>The person details with albums, or 404 if not found.</returns>
|
||||||
/// <returns>The person details.</returns>
|
|
||||||
[HttpGet("{id}")]
|
[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, uid));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
/// <param name="pagedSearch">Pagination, search, and sort parameters.</param>
|
||||||
/// <returns>A list of people previews.</returns>
|
/// <returns>A paginated list of person previews.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<List<PersonPreviewDto>> GetAll(PagedSearchParametersDto pagedSearch) {
|
public ActionResult<List<PersonPreviewDto>> GetAll([FromQuery] PersonSearchParametersDto pagedSearch) {
|
||||||
var people = personRepository.GetAll()
|
var uid = authService.GetUserData(User)?.Id;
|
||||||
.Select(p => new PersonPreviewDto { Id = p.Id, Name = p.Name })
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
.ToList();
|
|
||||||
|
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);
|
return Ok(people);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a person.
|
/// Updates a person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,8 +73,37 @@ public class PersonController(
|
|||||||
/// <param name="dto">The updated person data.</param>
|
/// <param name="dto">The updated person data.</param>
|
||||||
/// <returns>200 on success.</returns>
|
/// <returns>200 on success.</returns>
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] PersonUpdateDto dto) =>throw new NotImplementedException();
|
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>
|
/// <summary>
|
||||||
/// Updates multiple people in a bulk operation.
|
/// Updates multiple people in a bulk operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -55,25 +111,100 @@ public class PersonController(
|
|||||||
/// <returns>200 on success.</returns>
|
/// <returns>200 on success.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Admin, Curator")]
|
||||||
public IStatusCodeActionResult BulkUpdate([FromBody]BulkDto<PersonUpdateDto> personBulkDto)=>throw new NotImplementedException();
|
public IActionResult BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
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.Visibility = personBulkDto.Data.Visibility ?? person.Visibility;
|
||||||
|
person.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (personBulkDto.Data.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
|
||||||
|
personRepository.SetMaintainers(person.Id, personBulkDto.Data.MaintainerUserIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
personRepository.Save();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a person by ID.
|
/// Deletes a person by ID (soft delete).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The person ID.</param>
|
/// <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}")]
|
[HttpDelete("{id}")]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[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>
|
/// <summary>
|
||||||
/// Deletes multiple people in a bulk operation.
|
/// Deletes multiple people in a bulk operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ids">The list of person IDs to delete.</param>
|
/// <param name="ids">The list of person IDs to delete.</param>
|
||||||
/// <returns>200 on success.</returns>
|
/// <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]
|
[HttpDelete]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[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>
|
||||||
|
/// Merges multiple source people into a destination person.
|
||||||
|
/// Albums, faces, and maintainers are reassigned to the destination, then source people are hard-deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The merge request with destination and source IDs.</param>
|
||||||
|
/// <returns>200 on success, 400 if destination is in sources or not found.</returns>
|
||||||
|
[HttpPost("merge")]
|
||||||
|
[Authorize(Roles = "Admin, Curator")]
|
||||||
|
public IActionResult Merge([FromBody] PersonMergeDto dto) {
|
||||||
|
if (dto.SourceIds.Contains(dto.DestinationId))
|
||||||
|
return BadRequest("Destination person ID must not appear in source person IDs.");
|
||||||
|
|
||||||
|
if (personRepository.Find(dto.DestinationId) == null)
|
||||||
|
return NotFound("Destination person not found.");
|
||||||
|
|
||||||
|
personRepository.MergePeople(dto.DestinationId, dto.SourceIds);
|
||||||
|
personRepository.Save();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new person.
|
/// Creates a new person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -81,5 +212,15 @@ public class PersonController(
|
|||||||
/// <returns>200 with the new person ID.</returns>
|
/// <returns>200 with the new person ID.</returns>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -67,10 +67,10 @@ public class TagController(
|
|||||||
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
/// <param name="pagedSearch">Pagination and search parameters.</param>
|
||||||
/// <returns>A list of tags matching the search criteria.</returns>
|
/// <returns>A list of tags matching the search criteria.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<List<TagDto>> GetAll(PagedSearchParametersDto pagedSearch) {
|
public ActionResult<List<TagDto>> GetAll([FromQuery] PagedSearchParametersDto pagedSearch) {
|
||||||
pagedSearch.Search ??= string.Empty;
|
pagedSearch.Search ??= string.Empty;
|
||||||
if(pagedSearch.Page < 0) { return BadRequest("Page must be greater than 0"); }
|
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<Tag> tags = tagRepository.Search(pagedSearch.Search, pagedSearch.Page, pagedSearch.PageSize);
|
||||||
List<TagDto> tagsDto = [];
|
List<TagDto> tagsDto = [];
|
||||||
@@ -92,7 +92,7 @@ public class TagController(
|
|||||||
/// <param name="id">The tag ID.</param>
|
/// <param name="id">The tag ID.</param>
|
||||||
/// <param name="tagDto">The updated tag data.</param>
|
/// <param name="tagDto">The updated tag data.</param>
|
||||||
/// <returns>200 on success, 404 if not found, 409 if name conflict.</returns>
|
/// <returns>200 on success, 404 if not found, 409 if name conflict.</returns>
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto) {
|
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto) {
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ public class TagController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tagBulkDto">The bulk update data with IDs and shared update values.</param>
|
/// <param name="tagBulkDto">The bulk update data with IDs and shared update values.</param>
|
||||||
/// <returns>200 on success, or the first error response.</returns>
|
/// <returns>200 on success, or the first error response.</returns>
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<TagUpdateDto> tagBulkDto) {
|
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto<TagUpdateDto> tagBulkDto) {
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ public class TagController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The tag ID.</param>
|
/// <param name="id">The tag ID.</param>
|
||||||
/// <returns>200 on success, 404 if not found.</returns>
|
/// <returns>200 on success, 404 if not found.</returns>
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
public ActionResult Delete([FromRoute] Guid id) {
|
public ActionResult Delete([FromRoute] Guid id) {
|
||||||
IStatusCodeActionResult result = DeleteTag(id);
|
IStatusCodeActionResult result = DeleteTag(id);
|
||||||
@@ -142,7 +142,7 @@ public class TagController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ids">The list of tag IDs to delete.</param>
|
/// <param name="ids">The list of tag IDs to delete.</param>
|
||||||
/// <returns>200 on success, or the first error response.</returns>
|
/// <returns>200 on success, or the first error response.</returns>
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Maintainer,Curator,Admin")]
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
public IActionResult BulkDelete([FromBody] List<Guid> ids) {
|
public IActionResult BulkDelete([FromBody] List<Guid> ids) {
|
||||||
foreach (Guid id in ids) {
|
foreach (Guid id in ids) {
|
||||||
@@ -160,7 +160,7 @@ public class TagController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tagDto">The tag creation data.</param>
|
/// <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>
|
/// <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]
|
[HttpPut]
|
||||||
public ActionResult Create([FromBody] TagCreateDto tagDto) {
|
public ActionResult Create([FromBody] TagCreateDto tagDto) {
|
||||||
|
|
||||||
|
|||||||
@@ -83,34 +83,28 @@ namespace Lactose.Controllers {
|
|||||||
return Unauthorized("Unauthorized");
|
return Unauthorized("Unauthorized");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Lower level of access should fill less information of users
|
|
||||||
logger.LogDebug($"Sending user with id {id}");
|
logger.LogDebug($"Sending user with id {id}");
|
||||||
|
|
||||||
User? user = userRepository.Find(id);
|
User? user = userRepository.FindWithMaintainedPersons(id);
|
||||||
|
|
||||||
if (user is null) return NotFound();
|
if (user is null) return NotFound();
|
||||||
|
|
||||||
return Ok(user.ToGetUsersDto());
|
return Ok(user.ToGetUsersDto(authenticatedUser.AccessLevel, authenticatedUser.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all users.
|
/// Gets all users (admin/curator only).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of all users.</returns>
|
/// <returns>A list of all users.</returns>
|
||||||
|
[Authorize(Roles = "Admin,Curator")]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult<List<UserInfoDto>> GetAll() {
|
public ActionResult<List<UserInfoDto>> GetAll() {
|
||||||
logger.LogDebug("Sending Users list");
|
|
||||||
LactoseAuthenticatedUser? authenticatedUser = authService.GetUserData(User);
|
LactoseAuthenticatedUser? authenticatedUser = authService.GetUserData(User);
|
||||||
|
EAccessLevel viewerAccessLevel = authenticatedUser?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
if (authenticatedUser == null) {
|
logger.LogDebug("Sending Users list");
|
||||||
logger.LogWarning(@"Anonymous User hasn't enough permissions to create a User. it shouldn't be here");
|
|
||||||
return Unauthorized("Unauthorized");
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Lower level of access should fill less information of users
|
return Ok(userRepository.GetAll().Select(u => u.ToGetUsersDto(viewerAccessLevel, authenticatedUser?.Id)).ToList());
|
||||||
//if (false) return Unauthorized();
|
|
||||||
|
|
||||||
return Ok(userRepository.GetAll().Select(u => u.ToGetUsersDto()).ToList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +143,24 @@ namespace Lactose.Controllers {
|
|||||||
|
|
||||||
if (dto.Email != null) { user.Email = dto.Email; }
|
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 (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 (dto.IsDeleted != null) { user.DeletedAt = dto.IsDeleted.Value ? DateTime.UtcNow : null; }
|
||||||
|
|
||||||
@@ -159,10 +170,14 @@ namespace Lactose.Controllers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.AccessLevel != null) { user.AccessLevel = dto.AccessLevel.Value; }
|
if (dto.AccessLevel != null) { user.AccessLevel = dto.AccessLevel.Value; }
|
||||||
|
|
||||||
|
if (dto.MaintainedPersonIds != null) {
|
||||||
|
userRepository.SetMaintainedPersons(user.Id, dto.MaintainedPersonIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository.Save();
|
userRepository.Save();
|
||||||
return Ok(user.ToGetUsersDto());
|
return Ok(user.ToGetUsersDto(authenticatedUser.AccessLevel, authenticatedUser.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -186,7 +201,7 @@ namespace Lactose.Controllers {
|
|||||||
|
|
||||||
if (user == null) return NotFound();
|
if (user == null) return NotFound();
|
||||||
|
|
||||||
userRepository.Delete(user);
|
user.DeletedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
//TODO: eventually remove/hide all of the user content if required
|
//TODO: eventually remove/hide all of the user content if required
|
||||||
userRepository.Save();
|
userRepository.Save();
|
||||||
|
|||||||
@@ -131,5 +131,12 @@
|
|||||||
"Description": "Number of assets to process per sub-job batch.",
|
"Description": "Number of assets to process per sub-job batch.",
|
||||||
"Type": 1,
|
"Type": 1,
|
||||||
"DisplayType": 2
|
"DisplayType": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "System Uploader Id",
|
||||||
|
"Value": "",
|
||||||
|
"Description": "User ID to attribute scanner-created assets to. Leave empty for no default owner.",
|
||||||
|
"Type": 0,
|
||||||
|
"DisplayType": 9
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+8
-4
@@ -8,16 +8,20 @@ EXPOSE 5162
|
|||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
ARG SixLaborsLicenseKey
|
||||||
|
|
||||||
WORKDIR "/src"
|
WORKDIR "/src"
|
||||||
COPY ["Lactose/Lactose.csproj", "Lactose/"]
|
COPY ["Lactose/Lactose.csproj", "Lactose/"]
|
||||||
RUN dotnet restore "Lactose/Lactose.csproj"
|
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
|
||||||
|
dotnet restore "Lactose/Lactose.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/Lactose"
|
WORKDIR "/src/Lactose"
|
||||||
RUN dotnet build "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
|
||||||
|
dotnet build "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
FROM build AS publish
|
FROM build AS publish
|
||||||
ARG BUILD_CONFIGURATION=Release
|
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
|
||||||
RUN dotnet publish "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
dotnet publish "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter;
|
using Butter;
|
||||||
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
@@ -17,6 +18,7 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JobStatus JobStatus { get; }
|
public override JobStatus JobStatus { get; }
|
||||||
IAssetRepository assetRepository;
|
IAssetRepository assetRepository;
|
||||||
|
ISettingsRepository settingsRepository;
|
||||||
string workingPath;
|
string workingPath;
|
||||||
Guid folderId;
|
Guid folderId;
|
||||||
ILogger<FileSystemCrawlJob> logger;
|
ILogger<FileSystemCrawlJob> logger;
|
||||||
@@ -31,17 +33,20 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
/// <param name="workingPath">The base path to crawl.</param>
|
/// <param name="workingPath">The base path to crawl.</param>
|
||||||
/// <param name="logger">Logger instance.</param>
|
/// <param name="logger">Logger instance.</param>
|
||||||
/// <param name="assetRepository">The asset repository.</param>
|
/// <param name="assetRepository">The asset repository.</param>
|
||||||
|
/// <param name="settingsRepository">The settings repository.</param>
|
||||||
/// <param name="jobManager">The job manager.</param>
|
/// <param name="jobManager">The job manager.</param>
|
||||||
public FileSystemCrawlJob(
|
public FileSystemCrawlJob(
|
||||||
Guid folderId,
|
Guid folderId,
|
||||||
string workingPath,
|
string workingPath,
|
||||||
ILogger<FileSystemCrawlJob> logger,
|
ILogger<FileSystemCrawlJob> logger,
|
||||||
IAssetRepository assetRepository,
|
IAssetRepository assetRepository,
|
||||||
|
ISettingsRepository settingsRepository,
|
||||||
JobManager jobManager
|
JobManager jobManager
|
||||||
) {
|
) {
|
||||||
JobStatus = new(this);
|
JobStatus = new(this);
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.assetRepository = assetRepository;
|
this.assetRepository = assetRepository;
|
||||||
|
this.settingsRepository = settingsRepository;
|
||||||
this.workingPath = workingPath;
|
this.workingPath = workingPath;
|
||||||
this.folderId = folderId;
|
this.folderId = folderId;
|
||||||
this.jobManager = jobManager;
|
this.jobManager = jobManager;
|
||||||
@@ -64,6 +69,12 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
int steps,
|
int steps,
|
||||||
totalSteps;
|
totalSteps;
|
||||||
|
|
||||||
|
// Resolve system uploader from settings once
|
||||||
|
Guid? uploaderId = null;
|
||||||
|
var uploaderSetting = settingsRepository.Get(Settings.SystemUploaderId.AsString())?.Value;
|
||||||
|
if (Guid.TryParse(uploaderSetting, out var parsed))
|
||||||
|
uploaderId = parsed;
|
||||||
|
|
||||||
var files = dir.GetFiles();
|
var files = dir.GetFiles();
|
||||||
logger.LogDebug($"Found {files.Length} files.");
|
logger.LogDebug($"Found {files.Length} files.");
|
||||||
|
|
||||||
@@ -115,14 +126,21 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
} else if (file.LastWriteTimeUtc > (existing.UpdatedAt ?? existing.CreatedAt)) {
|
} else if (file.LastWriteTimeUtc > (existing.UpdatedAt ?? existing.CreatedAt)) {
|
||||||
existing.FileSize = file.Length;
|
existing.FileSize = file.Length;
|
||||||
existing.UpdatedAt = file.LastWriteTimeUtc;
|
existing.UpdatedAt = file.LastWriteTimeUtc;
|
||||||
|
if (existing.UploadedBy == null && uploaderId.HasValue)
|
||||||
|
existing.UploadedBy = uploaderId;
|
||||||
assetRepository.Update(existing);
|
assetRepository.Update(existing);
|
||||||
assetRepository.Save();
|
assetRepository.Save();
|
||||||
logger.LogInformation("Updated asset {Asset} — file modification detected.", existing.OriginalPath);
|
logger.LogInformation("Updated asset {Asset} — file modification detected.", existing.OriginalPath);
|
||||||
|
} else if (existing.UploadedBy == null && uploaderId.HasValue) {
|
||||||
|
existing.UploadedBy = uploaderId;
|
||||||
|
assetRepository.Update(existing);
|
||||||
|
assetRepository.Save();
|
||||||
|
logger.LogInformation("Backfilled UploadedBy for asset {Asset}.", existing.OriginalPath);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var asset = AssetFromPath(file.FullName);
|
var asset = AssetFromPath(file.FullName, uploaderId);
|
||||||
if (asset == null) continue;
|
if (asset == null) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -153,7 +171,7 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
JobStatus.Complete("Crawl completed successfully.");
|
JobStatus.Complete("Crawl completed successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset? AssetFromPath(string filePath) {
|
Asset? AssetFromPath(string filePath, Guid? uploaderId) {
|
||||||
logger.LogTrace("Loading asset from file path {filePath}", filePath);
|
logger.LogTrace("Loading asset from file path {filePath}", filePath);
|
||||||
// Get the file info
|
// Get the file info
|
||||||
var finfo = new FileInfo(filePath);
|
var finfo = new FileInfo(filePath);
|
||||||
@@ -176,7 +194,8 @@ public sealed class FileSystemCrawlJob : Job {
|
|||||||
OriginalPath = finfo.FullName,
|
OriginalPath = finfo.FullName,
|
||||||
OriginalFilename = finfo.Name,
|
OriginalFilename = finfo.Name,
|
||||||
Type = type.Value,
|
Type = type.Value,
|
||||||
IsPubliclyShared = false,
|
Visibility = EVisibility.Private,
|
||||||
|
UploadedBy = uploaderId,
|
||||||
CreatedAt = finfo.CreationTimeUtc,
|
CreatedAt = finfo.CreationTimeUtc,
|
||||||
UpdatedAt = finfo.LastWriteTimeUtc,
|
UpdatedAt = finfo.LastWriteTimeUtc,
|
||||||
MimeType = type.Value switch {
|
MimeType = type.Value switch {
|
||||||
|
|||||||
+18
-1
@@ -1,5 +1,4 @@
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
@@ -147,4 +146,22 @@ public abstract class Job {
|
|||||||
/// Raised when the job is done, regardless of the final status (Completed, Canceled, or Failed).
|
/// Raised when the job is done, regardless of the final status (Completed, Canceled, or Failed).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual event EventHandler<JobStatus>? Done;
|
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); }
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-11
@@ -3,7 +3,6 @@ using Butter.Settings;
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Mapper;
|
using Lactose.Mapper;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
using Lactose.Utils;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
@@ -46,25 +45,35 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns children of a given parent job — checks active jobs first, then the database.
|
/// Returns children of a given parent job — checks active jobs first, then the database.
|
||||||
|
/// Supports delta sync via the <paramref name="since"/> parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<JobStatusDto> GetChildren(Guid parentId) {
|
/// <param name="parentId">The parent job ID.</param>
|
||||||
var results = new List<JobStatusDto>();
|
/// <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
|
// Active children still in memory
|
||||||
foreach (var job in activeJobs.Values) {
|
var active = activeJobs.Values
|
||||||
if (job.ParentJobId == parentId) {
|
.Where(j => j.ParentJobId == parentId)
|
||||||
results.Add(job.ToJobStatusDto());
|
.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
|
// Past children in the database
|
||||||
try {
|
try {
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
var repo = scope.ServiceProvider.GetRequiredService<IJobRecordRepository>();
|
||||||
var records = repo.GetChildren(parentId);
|
var records = since is null
|
||||||
foreach (var record in records) {
|
? repo.GetChildren(parentId)
|
||||||
results.Add(record.ToJobStatusDto());
|
: repo.GetChildrenModifiedSince(parentId, since.Value);
|
||||||
}
|
results.AddRange(records.Where(r => !seenIds.Contains(r.Id)).Select(r => r.ToJobStatusDto()));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.LogWarning(ex, "Could not load children for parent {ParentId}", parentId);
|
logger.LogWarning(ex, "Could not load children for parent {ParentId}", parentId);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-17
@@ -38,6 +38,11 @@ public class JobStatus(Job job) {
|
|||||||
/// Gets the progress of the job as a value between 0 and 1.
|
/// Gets the progress of the job as a value between 0 and 1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Progress { get; private set; } = 0;
|
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>
|
/// <summary>
|
||||||
/// Returns a string representation of the job status.
|
/// Returns a string representation of the job status.
|
||||||
@@ -49,10 +54,11 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional start message.</param>
|
/// <param name="message">Optional start message.</param>
|
||||||
public void Start(string message = "Job started") {
|
public void Start(string message = "Job started") {
|
||||||
Status = EJobStatus.Running;
|
Status = EJobStatus.Running;
|
||||||
Started = DateTime.UtcNow;
|
Started = DateTime.UtcNow;
|
||||||
Progress = 0;
|
Progress = 0;
|
||||||
Message = message;
|
Message = message;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -60,8 +66,9 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional waiting message.</param>
|
/// <param name="message">Optional waiting message.</param>
|
||||||
public void Wait(string message = "Job waiting") {
|
public void Wait(string message = "Job waiting") {
|
||||||
Status = EJobStatus.Waiting;
|
Status = EJobStatus.Waiting;
|
||||||
Message = message;
|
Message = message;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -73,7 +80,8 @@ public class JobStatus(Job job) {
|
|||||||
if (progress < 0 || progress > 1)
|
if (progress < 0 || progress > 1)
|
||||||
throw new ArgumentOutOfRangeException(nameof(progress), "Progress must be between 0 and 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;
|
if (message != null) Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +90,10 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional completion message.</param>
|
/// <param name="message">Optional completion message.</param>
|
||||||
public void Complete(string? message = null) {
|
public void Complete(string? message = null) {
|
||||||
Status = EJobStatus.Completed;
|
Status = EJobStatus.Completed;
|
||||||
Finished = DateTime.UtcNow;
|
Finished = DateTime.UtcNow;
|
||||||
Progress = 1;
|
Progress = 1;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
if (message != null) Message = message;
|
if (message != null) Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +102,10 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional completion message.</param>
|
/// <param name="message">Optional completion message.</param>
|
||||||
public void CompleteWithErrors(string? message = null) {
|
public void CompleteWithErrors(string? message = null) {
|
||||||
Status = EJobStatus.CompletedWithErrors;
|
Status = EJobStatus.CompletedWithErrors;
|
||||||
Finished = DateTime.UtcNow;
|
Finished = DateTime.UtcNow;
|
||||||
Progress = 1;
|
Progress = 1;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
if (message != null) Message = message;
|
if (message != null) Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +114,9 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional failure message.</param>
|
/// <param name="message">Optional failure message.</param>
|
||||||
public void Fail(string? message = null) {
|
public void Fail(string? message = null) {
|
||||||
Status = EJobStatus.Failed;
|
Status = EJobStatus.Failed;
|
||||||
Finished = DateTime.UtcNow;
|
Finished = DateTime.UtcNow;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
if (message != null) Message = message;
|
if (message != null) Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +125,9 @@ public class JobStatus(Job job) {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Optional cancellation message.</param>
|
/// <param name="message">Optional cancellation message.</param>
|
||||||
public void Cancel(string? message = null) {
|
public void Cancel(string? message = null) {
|
||||||
Status = EJobStatus.Canceled;
|
Status = EJobStatus.Canceled;
|
||||||
Finished = DateTime.UtcNow;
|
Finished = DateTime.UtcNow;
|
||||||
|
LastChange = DateTime.UtcNow;
|
||||||
if (message != null) Message = message;
|
if (message != null) Message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
@@ -105,7 +104,13 @@ public sealed class MetadataJob : Job {
|
|||||||
|
|
||||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||||
Logger.LogInformation(msg);
|
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) {
|
async Task MasterJob(CancellationToken token) {
|
||||||
@@ -146,10 +151,8 @@ public sealed class MetadataJob : Job {
|
|||||||
job.Done += (o, _) => {
|
job.Done += (o, _) => {
|
||||||
var sub = (MetadataJob)o!;
|
var sub = (MetadataJob)o!;
|
||||||
lock (childJobs) { childJobs.Remove(sub); }
|
lock (childJobs) { childJobs.Remove(sub); }
|
||||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
|
||||||
Interlocked.Increment(ref failedAssets);
|
|
||||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||||
failedAssets += sub.failedAssets;
|
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||||
if (Interlocked.Decrement(ref pending) == 0)
|
if (Interlocked.Decrement(ref pending) == 0)
|
||||||
childTcs.TrySetResult();
|
childTcs.TrySetResult();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
using CoenM.ImageHash.HashAlgorithms;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using CoenM.ImageHash.HashAlgorithms;
|
|
||||||
using Lactose.Utils;
|
using Lactose.Utils;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
@@ -115,7 +114,13 @@ public sealed class PHashJob : Job {
|
|||||||
|
|
||||||
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
var msg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||||
Logger.LogInformation(msg);
|
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) {
|
async Task MasterJob(CancellationToken token) {
|
||||||
@@ -156,10 +161,8 @@ public sealed class PHashJob : Job {
|
|||||||
job.Done += (o, _) => {
|
job.Done += (o, _) => {
|
||||||
var sub = (PHashJob)o!;
|
var sub = (PHashJob)o!;
|
||||||
lock (childJobs) { childJobs.Remove(sub); }
|
lock (childJobs) { childJobs.Remove(sub); }
|
||||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
|
||||||
Interlocked.Increment(ref failedAssets);
|
|
||||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||||
failedAssets += sub.failedAssets;
|
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||||
if (Interlocked.Decrement(ref pending) == 0)
|
if (Interlocked.Decrement(ref pending) == 0)
|
||||||
childTcs.TrySetResult();
|
childTcs.TrySetResult();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Butter.Dtos.Settings;
|
|
||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
@@ -170,7 +168,13 @@ public class PreviewJob : Job {
|
|||||||
|
|
||||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||||
Logger.LogInformation(resultMsg);
|
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) {
|
async Task MasterJob(CancellationToken token) {
|
||||||
@@ -237,10 +241,8 @@ public class PreviewJob : Job {
|
|||||||
job.Done += (o, _) => {
|
job.Done += (o, _) => {
|
||||||
var sub = (PreviewJob)o!;
|
var sub = (PreviewJob)o!;
|
||||||
lock (childJobs) { childJobs.Remove(sub); }
|
lock (childJobs) { childJobs.Remove(sub); }
|
||||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
|
||||||
Interlocked.Increment(ref failedAssets);
|
|
||||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||||
failedAssets += sub.failedAssets;
|
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||||
if (Interlocked.Decrement(ref pending) == 0)
|
if (Interlocked.Decrement(ref pending) == 0)
|
||||||
childTcs.TrySetResult();
|
childTcs.TrySetResult();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Butter.Dtos.Settings;
|
|
||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
using Butter.Types;
|
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
@@ -170,7 +168,13 @@ public class ThumbnailJob : Job {
|
|||||||
|
|
||||||
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
var resultMsg = $"Processed {processedAssets:N0}/{Batch.Length:N0} assets, {failedAssets:N0} failed.";
|
||||||
Logger.LogInformation(resultMsg);
|
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) {
|
async Task MasterJob(CancellationToken token) {
|
||||||
@@ -237,10 +241,8 @@ public class ThumbnailJob : Job {
|
|||||||
job.Done += (o, _) => {
|
job.Done += (o, _) => {
|
||||||
var sub = (ThumbnailJob)o!;
|
var sub = (ThumbnailJob)o!;
|
||||||
lock (childJobs) { childJobs.Remove(sub); }
|
lock (childJobs) { childJobs.Remove(sub); }
|
||||||
if (sub.JobStatus.Status == EJobStatus.Failed)
|
|
||||||
Interlocked.Increment(ref failedAssets);
|
|
||||||
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
Interlocked.Add(ref processedAssets, sub.processedAssets);
|
||||||
failedAssets += sub.failedAssets;
|
Interlocked.Add(ref failedAssets, sub.failedAssets);
|
||||||
if (Interlocked.Decrement(ref pending) == 0)
|
if (Interlocked.Decrement(ref pending) == 0)
|
||||||
childTcs.TrySetResult();
|
childTcs.TrySetResult();
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-5
@@ -7,25 +7,24 @@
|
|||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<UserSecretsId>00b763fc-028f-4464-9478-fc1006aeffa8</UserSecretsId>
|
<UserSecretsId>00b763fc-028f-4464-9478-fc1006aeffa8</UserSecretsId>
|
||||||
<LangVersion>12</LangVersion>
|
<LangVersion>12</LangVersion>
|
||||||
|
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.15">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.OpenApi" Version="2.7.5" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
|
||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -43,6 +42,12 @@
|
|||||||
<ProjectReference Include="..\Butter\Butter.csproj" />
|
<ProjectReference Include="..\Butter\Butter.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Lactose.Analyzers\Lactose.Analyzers.csproj"
|
||||||
|
OutputItemType="Analyzer"
|
||||||
|
ReferenceOutputAssembly="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="DefaultSettings.json">
|
<None Include="DefaultSettings.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter.Dtos.Album;
|
using Butter.Dtos.Album;
|
||||||
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
|
||||||
namespace Lactose.Mapper;
|
namespace Lactose.Mapper;
|
||||||
@@ -8,44 +9,52 @@ namespace Lactose.Mapper;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AlbumMapper {
|
public static class AlbumMapper {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps an Album object to an AlbumFullDto object.
|
/// Maps an Album object to an AlbumPreviewDto with visibility-aware fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="album">The album to map.</param>
|
/// <param name="album">The album to map.</param>
|
||||||
/// <returns>An AlbumFullDto object.</returns>
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
public static AlbumFullDto ToAlbumFullDto(this Album album) => new AlbumFullDto {
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
|
/// <returns>An AlbumPreviewDto object.</returns>
|
||||||
|
public static AlbumPreviewDto ToAlbumPreviewDto(this Album album, EAccessLevel accessLevel, Guid? viewerId = null) => new AlbumPreviewDto {
|
||||||
Id = album.Id,
|
Id = album.Id,
|
||||||
Name = album.Title,
|
Name = album.Title,
|
||||||
Owner = album.UserOwnerId,
|
|
||||||
Person = album.PersonOwnerId,
|
Person = album.PersonOwnerId,
|
||||||
PersonName = album.PersonOwner?.Name,
|
PersonName = album.PersonOwner?.Name,
|
||||||
CoverAssetId = album.CoverAssetId,
|
CoverAssetId = album.CoverAssetId,
|
||||||
AssetCount = album.Assets?.Count ?? 0,
|
CoverWidth = album.CoverAsset?.ResolutionWidth,
|
||||||
Images = album.Assets!.Select(asset => asset.Id).ToList(),
|
CoverHeight = album.CoverAsset?.ResolutionHeight,
|
||||||
AssetPreviews = album.Assets!.Select(asset => new AlbumAssetPreviewDto {
|
AssetCount = album.Assets?.Count(a => a.DeletedAt == null) ?? 0,
|
||||||
|
Visibility = accessLevel >= EAccessLevel.Maintainer ? album.Visibility : null,
|
||||||
|
DeletedAt = 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>
|
||||||
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
|
/// <returns>An AlbumFullDto object.</returns>
|
||||||
|
public static AlbumFullDto ToAlbumFullDto(this Album album, EAccessLevel accessLevel, Guid? viewerId) => 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,
|
||||||
|
DeletedAt = null,
|
||||||
|
Images = album.Assets?.Select(asset => asset.Id).ToList() ?? [],
|
||||||
|
AssetPreviews = album.Assets?.Select(asset => new AlbumAssetPreviewDto {
|
||||||
Id = asset.Id,
|
Id = asset.Id,
|
||||||
ResolutionWidth = asset.ResolutionWidth,
|
ResolutionWidth = asset.ResolutionWidth,
|
||||||
ResolutionHeight = asset.ResolutionHeight,
|
ResolutionHeight = asset.ResolutionHeight,
|
||||||
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
||||||
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
||||||
FileName = asset.OriginalFilename
|
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||||
}).ToList()
|
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null,
|
||||||
|
DeletedAt = accessLevel >= EAccessLevel.Admin || asset.UploadedBy == viewerId ? asset.DeletedAt : 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,
|
|
||||||
PersonName = album.PersonOwner?.Name,
|
|
||||||
CoverAssetId = album.CoverAssetId,
|
|
||||||
AssetCount = album.Assets?.Count ?? 0
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter.Dtos.Asset;
|
using Butter.Dtos.Asset;
|
||||||
|
using Butter.Dtos.User;
|
||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
|
||||||
@@ -9,55 +10,68 @@ namespace Lactose.Mapper;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AssetsMapper {
|
public static class AssetsMapper {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps an Asset object to a GetFullAssetDto object.
|
/// Maps an Asset object to a AssetDto object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="asset">The asset to map.</param>
|
/// <param name="asset">The asset to map.</param>
|
||||||
/// <param name="accessLevel">The access level of the user.</param>
|
/// <param name="accessLevel">The access level of the user.</param>
|
||||||
/// <returns>A GetFullAssetDto object.</returns>
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
public static AssetDto ToFullAssetsDto(this Asset asset, EAccessLevel accessLevel) => new AssetDto {
|
/// <returns>A AssetDto object.</returns>
|
||||||
|
public static AssetDto ToFullAssetsDto(this Asset asset, EAccessLevel accessLevel, Guid? viewerId) => new AssetDto {
|
||||||
Id = asset.Id,
|
Id = asset.Id,
|
||||||
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||||
AssetType = asset.Type,
|
AssetType = asset.Type,
|
||||||
IsPublic = asset.IsPubliclyShared,
|
|
||||||
CreatedAt = asset.CreatedAt,
|
CreatedAt = asset.CreatedAt,
|
||||||
UpdatedAt = asset.UpdatedAt,
|
UpdatedAt = asset.UpdatedAt,
|
||||||
DeletedAt = asset.DeletedAt,
|
DeletedAt = accessLevel >= EAccessLevel.Admin || asset.UploadedBy == viewerId ? asset.DeletedAt : null,
|
||||||
Owner = asset.Owner!.ToGetUsersDto(),
|
UploadedBy = asset.Uploader?.ToGetUsersDto(accessLevel, viewerId)
|
||||||
|
?? new UserInfoDto { Id = Guid.Empty, Username = "deleted", AccessLevel = EAccessLevel.User },
|
||||||
MimeType = asset.MimeType,
|
MimeType = asset.MimeType,
|
||||||
ResolutionWidth = asset.ResolutionWidth,
|
ResolutionWidth = asset.ResolutionWidth,
|
||||||
ResolutionHeight = asset.ResolutionHeight,
|
ResolutionHeight = asset.ResolutionHeight,
|
||||||
FileSize = asset.FileSize,
|
FileSize = asset.FileSize,
|
||||||
Duration = asset.Duration ?? 0,
|
Duration = asset.Duration ?? 0,
|
||||||
FrameRate = asset.FrameRate ?? 0
|
FrameRate = asset.FrameRate ?? 0,
|
||||||
|
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps an Asset object to a GetAssetPreviewDto object.
|
/// Maps an Asset object to a AssetPreviewDto object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="asset">The asset to map.</param>
|
/// <param name="asset">The asset to map.</param>
|
||||||
/// <returns>A GetAssetPreviewDto object.</returns>
|
/// <param name="accessLevel">The access level of the user.</param>
|
||||||
public static AssetPreviewDto ToAssetPreviewDto(this Asset asset) => new AssetPreviewDto {
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
|
/// <returns>A AssetPreviewDto object.</returns>
|
||||||
|
public static AssetPreviewDto ToAssetPreviewDto(this Asset asset, EAccessLevel accessLevel, Guid? viewerId) => new AssetPreviewDto {
|
||||||
Id = asset.Id,
|
Id = asset.Id,
|
||||||
MimeType = asset.MimeType,
|
MimeType = asset.MimeType,
|
||||||
ResolutionWidth = asset.ResolutionWidth,
|
ResolutionWidth = asset.ResolutionWidth,
|
||||||
ResolutionHeight = asset.ResolutionHeight,
|
ResolutionHeight = asset.ResolutionHeight,
|
||||||
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
HasThumbnail = !string.IsNullOrEmpty(asset.ThumbnailPath),
|
||||||
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
HasPreview = !string.IsNullOrEmpty(asset.PreviewPath),
|
||||||
FileName = asset.OriginalFilename,
|
FileName = accessLevel >= EAccessLevel.Curator ? asset.OriginalFilename : null,
|
||||||
AlbumNames = asset.Albums?.Select(a => a.Title).Distinct().ToList(),
|
AlbumNames = asset.Albums?.Select(a => a.Title).Distinct().ToList(),
|
||||||
AlbumIds = asset.Albums?.Select(a => a.Id).Distinct().ToList(),
|
AlbumIds = asset.Albums?.Select(a => a.Id).Distinct().ToList(),
|
||||||
CosplayerNames = asset.Albums?
|
CosplayerNames = asset.Albums?
|
||||||
.Where(a => a.PersonOwner != null)
|
.Where(a => a.PersonOwner != null)
|
||||||
.Select(a => a.PersonOwner!.Name)
|
.Select(a => a.PersonOwner!.Name)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList()
|
.ToList(),
|
||||||
|
CosplayerIds = asset.Albums?
|
||||||
|
.Where(a => a.PersonOwner != null)
|
||||||
|
.Select(a => a.PersonOwner!.Id)
|
||||||
|
.Distinct()
|
||||||
|
.ToList(),
|
||||||
|
Visibility = accessLevel >= EAccessLevel.Maintainer ? asset.Visibility : null,
|
||||||
|
DeletedAt = accessLevel >= EAccessLevel.Admin || asset.UploadedBy == viewerId ? asset.DeletedAt : null
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="assets">The collection of assets to map.</param>
|
/// <param name="assets">The collection of assets to map.</param>
|
||||||
/// <returns>A collection of GetAssetPreviewDto objects.</returns>
|
/// <param name="accessLevel">The access level of the user.</param>
|
||||||
public static IEnumerable<AssetPreviewDto> ToAssetPreviewDto(this IEnumerable<Asset> assets)
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
=> assets.Select(ToAssetPreviewDto);
|
/// <returns>A collection of AssetPreviewDto objects.</returns>
|
||||||
|
public static IEnumerable<AssetPreviewDto> ToAssetPreviewDto(this IEnumerable<Asset> assets, EAccessLevel accessLevel, Guid? viewerId)
|
||||||
|
=> assets.Select(a => a.ToAssetPreviewDto(accessLevel, viewerId));
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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>
|
||||||
|
/// <param name="viewerId">The ID of the requesting user.</param>
|
||||||
|
/// <returns>A PersonDetailedDto.</returns>
|
||||||
|
public static PersonDetailedDto ToPersonDetailedDto(this Person person, EAccessLevel accessLevel, Guid? viewerId = null) {
|
||||||
|
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 = person.AlbumTotalCount > 0 ? person.AlbumTotalCount : 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, viewerId)).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.Dtos.User;
|
||||||
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
|
||||||
namespace Lactose.Mapper;
|
namespace Lactose.Mapper;
|
||||||
@@ -8,22 +9,33 @@ namespace Lactose.Mapper;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UsersMapper {
|
public static class UsersMapper {
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="user">The User object to map.</param>
|
/// <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>
|
/// <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 {
|
return new UserInfoDto {
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Email = user.Email,
|
Username = user.Username,
|
||||||
Username = user.Username,
|
Email = showSensitive ? user.Email : null,
|
||||||
CreatedAt = user.CreatedAt,
|
CreatedAt = user.CreatedAt,
|
||||||
IsBanned = user.BannedAt != null,
|
UpdatedAt = showSensitive ? user.UpdatedAt : null,
|
||||||
BannedAt = user.BannedAt,
|
LastLogin = showSensitive ? user.LastLogin : null,
|
||||||
LastLogin = user.LastLogin,
|
BannedAt = showSensitive ? user.BannedAt : null,
|
||||||
UpdatedAt = user.UpdatedAt,
|
DeletedAt = isAdmin ? user.DeletedAt : null,
|
||||||
DeletedAt = user.DeletedAt,
|
IsBanned = showSensitive && user.BannedAt != null,
|
||||||
AccessLevel = user.AccessLevel,
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,597 @@
|
|||||||
|
// <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("20260716185022_AddUnlinkedAssetFilters")]
|
||||||
|
partial class AddUnlinkedAssetFilters
|
||||||
|
{
|
||||||
|
/// <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("OriginalFilename")
|
||||||
|
.HasDatabaseName("IX_Assets_OriginalFilename_Trgm");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("OriginalFilename"), "gin");
|
||||||
|
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("OriginalFilename"), new[] { "gin_trgm_ops" });
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUnlinkedAssetFilters : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Assets_OriginalFilename_Trgm",
|
||||||
|
table: "Assets",
|
||||||
|
column: "OriginalFilename")
|
||||||
|
.Annotation("Npgsql:IndexMethod", "gin")
|
||||||
|
.Annotation("Npgsql:IndexOperators", new[] { "gin_trgm_ops" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Assets_OriginalFilename_Trgm",
|
||||||
|
table: "Assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ namespace Lactose.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "10.0.9")
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm");
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
@@ -54,21 +55,6 @@ namespace Lactose.Migrations
|
|||||||
b.ToTable("AssetTag");
|
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 =>
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -79,7 +65,7 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<Guid?>("PersonOwnerId")
|
b.Property<Guid?>("PersonOwnerId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
@@ -89,10 +75,10 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("VARCHAR(255)");
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<Guid?>("UserOwnerId")
|
b.Property<int>("Visibility")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
@@ -100,7 +86,11 @@ namespace Lactose.Migrations
|
|||||||
|
|
||||||
b.HasIndex("PersonOwnerId");
|
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");
|
b.ToTable("Albums");
|
||||||
});
|
});
|
||||||
@@ -112,10 +102,10 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
b.Property<DateTime?>("DeletedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<float?>("Duration")
|
b.Property<float?>("Duration")
|
||||||
.HasColumnType("real");
|
.HasColumnType("real");
|
||||||
@@ -133,9 +123,6 @@ namespace Lactose.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("bit(64)");
|
.HasColumnType("bit(64)");
|
||||||
|
|
||||||
b.Property<bool>("IsPubliclyShared")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("MimeType")
|
b.Property<string>("MimeType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(255)");
|
.HasColumnType("VARCHAR(255)");
|
||||||
@@ -148,9 +135,6 @@ namespace Lactose.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(2048)");
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
b.Property<Guid?>("OwnerId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("PreviewFormat")
|
b.Property<string>("PreviewFormat")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(16)");
|
.HasColumnType("VARCHAR(16)");
|
||||||
@@ -183,16 +167,32 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
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");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("FolderId");
|
b.HasIndex("FolderId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginalFilename")
|
||||||
|
.HasDatabaseName("IX_Assets_OriginalFilename_Trgm");
|
||||||
|
|
||||||
|
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("OriginalFilename"), "gin");
|
||||||
|
NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("OriginalFilename"), new[] { "gin_trgm_ops" });
|
||||||
|
|
||||||
b.HasIndex("OriginalPath")
|
b.HasIndex("OriginalPath")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.HasIndex("OwnerId");
|
b.HasIndex("UploadedBy");
|
||||||
|
|
||||||
|
b.HasIndex("Visibility", "UploadedBy")
|
||||||
|
.HasDatabaseName("IX_Assets_VisibleForSearch")
|
||||||
|
.HasFilter("\"DeletedAt\" IS NULL");
|
||||||
|
|
||||||
b.ToTable("Assets");
|
b.ToTable("Assets");
|
||||||
});
|
});
|
||||||
@@ -265,10 +265,10 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("Created")
|
b.Property<DateTime>("Created")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime?>("Finished")
|
b.Property<DateTime?>("Finished")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<int>("JobType")
|
b.Property<int>("JobType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
@@ -276,6 +276,9 @@ namespace Lactose.Migrations
|
|||||||
b.Property<string>("Message")
|
b.Property<string>("Message")
|
||||||
.HasColumnType("VARCHAR(2048)");
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ModifiedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(2048)");
|
.HasColumnType("VARCHAR(2048)");
|
||||||
@@ -287,7 +290,7 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("real");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b.Property<DateTime?>("Started")
|
b.Property<DateTime?>("Started")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
@@ -304,23 +307,58 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp without time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(255)");
|
.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")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Visibility")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.HasKey("Id");
|
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");
|
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 =>
|
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
@@ -379,20 +417,20 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<DateTime?>("BannedAt")
|
b.Property<DateTime?>("BannedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
b.Property<DateTime?>("DeletedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Email")
|
b.Property<string>("Email")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("VARCHAR(128)");
|
.HasColumnType("VARCHAR(128)");
|
||||||
|
|
||||||
b.Property<DateTime?>("LastLogin")
|
b.Property<DateTime?>("LastLogin")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Password")
|
b.Property<string>("Password")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -403,10 +441,10 @@ namespace Lactose.Migrations
|
|||||||
.HasColumnType("VARCHAR(255)");
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
b.Property<DateTime?>("RefreshTokenExpires")
|
b.Property<DateTime?>("RefreshTokenExpires")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Username")
|
b.Property<string>("Username")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -447,21 +485,6 @@ namespace Lactose.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
b.HasOne("Lactose.Models.Asset", "CoverAsset")
|
||||||
@@ -472,15 +495,9 @@ namespace Lactose.Migrations
|
|||||||
.WithMany("Albums")
|
.WithMany("Albums")
|
||||||
.HasForeignKey("PersonOwnerId");
|
.HasForeignKey("PersonOwnerId");
|
||||||
|
|
||||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
|
||||||
.WithMany("OwnedAlbums")
|
|
||||||
.HasForeignKey("UserOwnerId");
|
|
||||||
|
|
||||||
b.Navigation("CoverAsset");
|
b.Navigation("CoverAsset");
|
||||||
|
|
||||||
b.Navigation("PersonOwner");
|
b.Navigation("PersonOwner");
|
||||||
|
|
||||||
b.Navigation("UserOwner");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
@@ -489,13 +506,13 @@ namespace Lactose.Migrations
|
|||||||
.WithMany("Assets")
|
.WithMany("Assets")
|
||||||
.HasForeignKey("FolderId");
|
.HasForeignKey("FolderId");
|
||||||
|
|
||||||
b.HasOne("Lactose.Models.User", "Owner")
|
b.HasOne("Lactose.Models.User", "Uploader")
|
||||||
.WithMany("OwnedAssets")
|
.WithMany("UploadedAssets")
|
||||||
.HasForeignKey("OwnerId");
|
.HasForeignKey("UploadedBy");
|
||||||
|
|
||||||
b.Navigation("Folder");
|
b.Navigation("Folder");
|
||||||
|
|
||||||
b.Navigation("Owner");
|
b.Navigation("Uploader");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
@@ -513,6 +530,34 @@ namespace Lactose.Migrations
|
|||||||
b.Navigation("Person");
|
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 =>
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||||
@@ -541,9 +586,7 @@ namespace Lactose.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("OwnedAlbums");
|
b.Navigation("UploadedAssets");
|
||||||
|
|
||||||
b.Navigation("OwnedAssets");
|
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -1,3 +1,4 @@
|
|||||||
|
using Butter.Types;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
@@ -30,12 +31,6 @@ public class Album {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the user owner identifier.
|
|
||||||
/// </summary>
|
|
||||||
[ForeignKey(nameof(UserOwner))]
|
|
||||||
public Guid? UserOwnerId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person owner identifier.
|
/// Gets or sets the person owner identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -48,13 +43,13 @@ public class Album {
|
|||||||
[ForeignKey(nameof(CoverAsset))]
|
[ForeignKey(nameof(CoverAsset))]
|
||||||
public Guid? CoverAssetId { get; set; }
|
public Guid? CoverAssetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the visibility level of the album.
|
||||||
|
/// </summary>
|
||||||
|
public EVisibility Visibility { get; set; }
|
||||||
|
|
||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the user owner of the album. This is the user account who created/owns the album.
|
|
||||||
/// </summary>
|
|
||||||
public User? UserOwner { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the person owner of the album. This is the person/model who appears in the album.
|
/// Gets or sets the person owner of the album. This is the person/model who appears in the album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+8
-13
@@ -75,15 +75,15 @@ public class Asset {
|
|||||||
public required EAssetType Type { get; set; }
|
public required EAssetType Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the owner ID of the asset.
|
/// Gets or sets the ID of the user who uploaded the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ForeignKey(nameof(Owner))]
|
[ForeignKey(nameof(UploadedBy))]
|
||||||
public Guid? OwnerId { get; set; }
|
public Guid? UploadedBy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the asset is publicly shared.
|
/// Gets or sets the visibility level of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPubliclyShared { get; set; }
|
public EVisibility Visibility { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the creation date of the asset.
|
/// Gets or sets the creation date of the asset.
|
||||||
@@ -136,7 +136,7 @@ public class Asset {
|
|||||||
/// Computed hash of the asset.
|
/// Computed hash of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required][Column(TypeName = "bit(64)")]
|
[Required][Column(TypeName = "bit(64)")]
|
||||||
public BitArray Hash { get; set; }
|
public BitArray Hash { get; set; } = new BitArray(64);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or Sets which folder the asset is in.
|
/// Gets or Sets which folder the asset is in.
|
||||||
@@ -147,14 +147,9 @@ public class Asset {
|
|||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the owner of the asset.
|
/// Gets or sets the user who uploaded the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public User? Owner { get; set; }
|
public User? Uploader { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the list of users with whom the asset is shared.
|
|
||||||
/// </summary>
|
|
||||||
public List<User>? SharedWith { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of albums associated with the asset.
|
/// 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).
|
/// Gets or sets the job progress (0 to 1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Progress { get; set; }
|
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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Butter.Types;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
@@ -31,12 +32,37 @@ public class Person {
|
|||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the date and time when the person record was deleted.
|
/// Gets or sets the asset ID used as the profile picture for this person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? DeletedAt { get; set; }
|
public Guid? ProfileAssetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the horizontal crop offset for the profile picture (0–100, default 50 = center).
|
||||||
|
/// </summary>
|
||||||
|
public float? ProfileCropX { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the vertical crop offset for the profile picture (0–100, default 50 = center).
|
||||||
|
/// </summary>
|
||||||
|
public float? ProfileCropY { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the zoom level for the profile picture (1.0 = fit, higher = more zoomed in).
|
||||||
|
/// </summary>
|
||||||
|
public float? ProfileCropZoom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the visibility level of the person.
|
||||||
|
/// </summary>
|
||||||
|
public EVisibility Visibility { get; set; }
|
||||||
|
|
||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the profile picture asset.
|
||||||
|
/// </summary>
|
||||||
|
public Asset? ProfileAsset { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of faces associated with the person.
|
/// Gets or sets the list of faces associated with the person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -47,5 +73,17 @@ public class Person {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Album>? Albums { get; set; }
|
public List<Album>? Albums { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of users who maintain this cosplayer.
|
||||||
|
/// </summary>
|
||||||
|
public List<User>? Maintainers { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the total number of albums (before pagination).
|
||||||
|
/// Populated by <c>FindVisible</c>; ignored by EF Core.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public int AlbumTotalCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Lactose.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the many-to-many relationship between maintainer users and the cosplayers they maintain.
|
||||||
|
/// </summary>
|
||||||
|
[PrimaryKey(nameof(PersonId), nameof(UserId))]
|
||||||
|
public class PersonMaintainer {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the person (cosplayer) ID.
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(Person))]
|
||||||
|
public Guid PersonId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the person (cosplayer).
|
||||||
|
/// </summary>
|
||||||
|
public Person Person { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maintainer user ID.
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(User))]
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maintainer user.
|
||||||
|
/// </summary>
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -78,19 +78,14 @@ public class User {
|
|||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of assets owned by the user.
|
/// Gets or sets the list of assets uploaded by the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Asset>? OwnedAssets { get; set; }
|
public List<Asset>? UploadedAssets { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of albums owned by the user.
|
/// Gets or sets the list of people (cosplayers) maintained by this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Album>? OwnedAlbums { get; set; }
|
public List<Person>? MaintainedPersons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the list of assets shared with the user.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<Asset>? SharedAssets { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-7
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
using Lactose.Authorization;
|
using Lactose.Authorization;
|
||||||
using Lactose.Configuration;
|
using Lactose.Configuration;
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
@@ -9,17 +11,13 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.OpenApi;
|
using Microsoft.OpenApi;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Pgvector.EntityFrameworkCore;
|
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
//Adds configurations to the builder
|
|
||||||
builder.Configuration.AddJsonFile("appsettings.json", true, true)
|
|
||||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true);
|
|
||||||
|
|
||||||
builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||||
|
|
||||||
// CORS: allow the Blazor WASM frontend to call this API from the browser.
|
// CORS: allow the Blazor WASM frontend to call this API from the browser.
|
||||||
@@ -49,6 +47,68 @@ builder.Services.AddCors(options => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.ConfigureOptions<SignKeyProvider>();
|
builder.Services.ConfigureOptions<SignKeyProvider>();
|
||||||
|
builder.Services.ConfigureOptions<RateLimitBucketOptionsProvider>();
|
||||||
|
builder.Services.AddRateLimiter(options => {
|
||||||
|
options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
|
||||||
|
|
||||||
|
options.OnRejected = (context, cancellationToken) => {
|
||||||
|
context.HttpContext.Response.ContentType = "application/json";
|
||||||
|
var retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfterValue)
|
||||||
|
? (int)retryAfterValue.TotalSeconds
|
||||||
|
: 60;
|
||||||
|
context.HttpContext.Response.Headers.RetryAfter = retryAfter.ToString();
|
||||||
|
return new ValueTask(context.HttpContext.Response.WriteAsJsonAsync(new {
|
||||||
|
error = "rate limit exceeded",
|
||||||
|
retryAfter
|
||||||
|
}, cancellationToken));
|
||||||
|
};
|
||||||
|
|
||||||
|
var bucketConfig = builder.Configuration
|
||||||
|
.GetSection(RateLimitBucketOptions.SectionName)
|
||||||
|
.Get<RateLimitBucketOptions>() ?? new RateLimitBucketOptions();
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("global", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.Global.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.Global.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.Global.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("auth", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.Auth.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.Auth.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.Auth.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("media_original", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.MediaOriginal.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.MediaOriginal.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.MediaOriginal.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("media_thumb", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.MediaThumb.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.MediaThumb.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.MediaThumb.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("media_preview", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.MediaPreview.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.MediaPreview.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.MediaPreview.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddFixedWindowLimiter("jobs", opt => {
|
||||||
|
opt.PermitLimit = bucketConfig.Jobs.PermitLimit;
|
||||||
|
opt.Window = TimeSpan.FromSeconds(bucketConfig.Jobs.WindowSeconds);
|
||||||
|
opt.QueueLimit = bucketConfig.Jobs.QueueLimit ?? 0;
|
||||||
|
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
string? user = builder.Configuration["DatabaseCredentials:UserID"];
|
string? user = builder.Configuration["DatabaseCredentials:UserID"];
|
||||||
string? password = builder.Configuration["DatabaseCredentials:Password"];
|
string? password = builder.Configuration["DatabaseCredentials:Password"];
|
||||||
@@ -86,8 +146,6 @@ builder.Services.AddDbContext<LactoseDbContext>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
|
||||||
|
|
||||||
// Add services to initialize an empty the database
|
// Add services to initialize an empty the database
|
||||||
builder.Services.AddScoped<IDbInitializer, DbInitializer>();
|
builder.Services.AddScoped<IDbInitializer, DbInitializer>();
|
||||||
|
|
||||||
@@ -181,6 +239,7 @@ if (app.Environment.IsDevelopment()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseCors("MilkStreamPolicy");
|
app.UseCors("MilkStreamPolicy");
|
||||||
|
app.UseRateLimiter();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using Butter.Dtos;
|
||||||
|
using Butter.Dtos.Album;
|
||||||
|
using Butter.Types;
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
|
using Lactose.Mapper;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
@@ -24,14 +27,82 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Album> SearchQuery(string query, int page = 0, int pageSize = 150) {
|
public IEnumerable<AlbumPreviewDto> SearchQuery(string query, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User, Guid? uploadedBy = null) {
|
||||||
return context.Albums
|
IQueryable<Album> albumsQuery = context.Albums;
|
||||||
.Include(a => a.CoverAsset)
|
|
||||||
|
if (!string.IsNullOrEmpty(query))
|
||||||
|
albumsQuery = albumsQuery.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"));
|
||||||
|
|
||||||
|
if (unassigned)
|
||||||
|
albumsQuery = albumsQuery.Where(a => a.PersonOwnerId == null);
|
||||||
|
|
||||||
|
if (uploadedBy.HasValue)
|
||||||
|
albumsQuery = albumsQuery.Where(a => a.Assets!.Any(asset => asset.UploadedBy == uploadedBy.Value));
|
||||||
|
|
||||||
|
// Apply visibility filter scoped to access level
|
||||||
|
albumsQuery = accessLevel switch {
|
||||||
|
< EAccessLevel.Maintainer => albumsQuery.Where(a => a.Visibility <= EVisibility.Protected),
|
||||||
|
EAccessLevel.Maintainer => albumsQuery.Where(a => a.Visibility <= EVisibility.Protected
|
||||||
|
|| context.PersonMaintainers.Any(pm => pm.UserId == userId && pm.PersonId == a.PersonOwnerId)),
|
||||||
|
_ => albumsQuery
|
||||||
|
};
|
||||||
|
|
||||||
|
IOrderedQueryable<Album> ordered = sortBy?.ToLower() switch
|
||||||
|
{
|
||||||
|
"name" => sortAsc ? albumsQuery.OrderBy(a => a.Title) : albumsQuery.OrderByDescending(a => a.Title),
|
||||||
|
"updated" => sortAsc ? albumsQuery.OrderBy(a => a.UpdatedAt) : albumsQuery.OrderByDescending(a => a.UpdatedAt),
|
||||||
|
"assets" => sortAsc
|
||||||
|
? albumsQuery.OrderBy(a => a.Assets!.Count)
|
||||||
|
: albumsQuery.OrderByDescending(a => a.Assets!.Count),
|
||||||
|
"person" => sortAsc ? albumsQuery.OrderBy(a => a.PersonOwner!.Name) : albumsQuery.OrderByDescending(a => a.PersonOwner!.Name),
|
||||||
|
_ => sortAsc ? albumsQuery.OrderBy(a => a.CreatedAt) : albumsQuery.OrderByDescending(a => a.CreatedAt),
|
||||||
|
};
|
||||||
|
|
||||||
|
var pageOffset = page * pageSize;
|
||||||
|
var albumIds = ordered
|
||||||
|
.Skip(pageOffset)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(a => a.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (albumIds.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var pagedAlbums = context.Albums
|
||||||
.Include(a => a.PersonOwner)
|
.Include(a => a.PersonOwner)
|
||||||
.Include(a => a.Assets)!.ThenInclude(a => a.SharedWith)
|
.Include(a => a.CoverAsset)
|
||||||
.Where(x => x.Title.Contains(query))
|
.Where(a => albumIds.Contains(a.Id))
|
||||||
.Skip(page * pageSize)
|
.ToList();
|
||||||
.Take(pageSize);
|
|
||||||
|
// Compute visible asset count per album at the DB level
|
||||||
|
Dictionary<Guid, int> assetCounts;
|
||||||
|
if (accessLevel >= EAccessLevel.Curator) {
|
||||||
|
assetCounts = context.Albums
|
||||||
|
.Where(a => albumIds.Contains(a.Id))
|
||||||
|
.Select(a => new { a.Id, Count = a.Assets!.Count() })
|
||||||
|
.ToDictionary(x => x.Id, x => x.Count);
|
||||||
|
} else {
|
||||||
|
assetCounts = context.Albums
|
||||||
|
.Where(a => albumIds.Contains(a.Id))
|
||||||
|
.Select(a => new {
|
||||||
|
a.Id,
|
||||||
|
Count = a.Assets!.Count(asset => asset.DeletedAt == null && (
|
||||||
|
asset.Visibility == EVisibility.Public ||
|
||||||
|
(asset.Visibility == EVisibility.Protected && userId != default) ||
|
||||||
|
(asset.Visibility == EVisibility.Private && asset.UploadedBy == userId)
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.ToDictionary(x => x.Id, x => x.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dtos = pagedAlbums.Select(a => {
|
||||||
|
var dto = a.ToAlbumPreviewDto(accessLevel, userId);
|
||||||
|
dto.AssetCount = assetCounts.GetValueOrDefault(a.Id, 0);
|
||||||
|
return dto;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var orderMap = albumIds.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);
|
||||||
|
return [.. dtos.OrderBy(d => orderMap.GetValueOrDefault(d.Id))];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -39,13 +110,62 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Album? Find(Guid id) => context.Albums
|
public Album? Find(Guid id) => context.Albums
|
||||||
|
.AsSplitQuery()
|
||||||
.Include(a => a.CoverAsset)
|
.Include(a => a.CoverAsset)
|
||||||
.Include(a => a.PersonOwner)
|
.Include(a => a.PersonOwner)
|
||||||
.Include(a => a.Assets)!.ThenInclude(a => a.SharedWith)
|
.Include(a => a.Assets)
|
||||||
.FirstOrDefault(a => a.Id == id);
|
.FirstOrDefault(a => a.Id == id);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Album> FindByOwner(Guid ownerId) => context.Albums.Where(a => a.UserOwnerId == ownerId);
|
public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) {
|
||||||
|
var album = context.Albums
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.CoverAsset)
|
||||||
|
.Include(a => a.PersonOwner)
|
||||||
|
.Include(a => a.Assets)
|
||||||
|
.FirstOrDefault(a => a.Id == id);
|
||||||
|
|
||||||
|
if (album == null) return null;
|
||||||
|
|
||||||
|
// R3: Gate album visibility
|
||||||
|
bool canSeeAlbum = accessLevel switch {
|
||||||
|
< EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected,
|
||||||
|
EAccessLevel.Maintainer when userId.HasValue => album.Visibility <= EVisibility.Protected
|
||||||
|
|| (album.PersonOwnerId.HasValue && context.PersonMaintainers.Any(pm =>
|
||||||
|
pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value)),
|
||||||
|
EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!canSeeAlbum) return null;
|
||||||
|
|
||||||
|
if (album.Assets == null) return album;
|
||||||
|
|
||||||
|
album.Assets = accessLevel switch {
|
||||||
|
EAccessLevel.Admin => [.. album.Assets.OrderBy(a => a.OriginalFilename)],
|
||||||
|
EAccessLevel.Curator => album.Assets.Where(a => a.DeletedAt == null || a.UploadedBy == userId).OrderBy(a => a.OriginalFilename).ToList(),
|
||||||
|
EAccessLevel.Maintainer when userId.HasValue => [.. FilterAssetsForMaintainer(album, userId.Value).OrderBy(a => a.OriginalFilename)],
|
||||||
|
_ => album.Assets.Where(a => a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
(a.Visibility == EVisibility.Protected && userId.HasValue) ||
|
||||||
|
(a.Visibility == EVisibility.Private && a.UploadedBy == userId)
|
||||||
|
)).OrderBy(a => a.OriginalFilename).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Asset> FilterAssetsForMaintainer(Album album, Guid userId) {
|
||||||
|
if (album.PersonOwnerId.HasValue && context.PersonMaintainers.Any(pm =>
|
||||||
|
pm.UserId == userId && pm.PersonId == album.PersonOwnerId.Value))
|
||||||
|
return album.Assets!.Where(a => a.DeletedAt == null).ToList();
|
||||||
|
|
||||||
|
return album.Assets!.Where(a => a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
a.Visibility == EVisibility.Protected ||
|
||||||
|
a.UploadedBy == userId
|
||||||
|
)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Album> FindByPerson(Guid personId) => context.Albums.Where(a => a.PersonOwnerId == personId);
|
public IEnumerable<Album> FindByPerson(Guid personId) => context.Albums.Where(a => a.PersonOwnerId == personId);
|
||||||
@@ -57,6 +177,33 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
|||||||
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
|
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
|
||||||
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void MergeAlbums(Guid destId, List<Guid> sourceIds) {
|
||||||
|
var dest = context.Albums
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Assets)
|
||||||
|
.FirstOrDefault(a => a.Id == destId);
|
||||||
|
if (dest == null) return;
|
||||||
|
|
||||||
|
var sources = context.Albums
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Assets)
|
||||||
|
.Where(a => sourceIds.Contains(a.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var existingAssetIds = dest.Assets?.Select(a => a.Id).ToHashSet() ?? [];
|
||||||
|
|
||||||
|
foreach (var source in sources) {
|
||||||
|
if (source.Assets == null) continue;
|
||||||
|
foreach (var asset in source.Assets) {
|
||||||
|
if (existingAssetIds.Add(asset.Id))
|
||||||
|
dest.Assets?.Add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Albums.RemoveRange(sources);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Remove(Album album) => context.Albums.Remove(album);
|
public void Remove(Album album) => context.Albums.Remove(album);
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,58 @@
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
|
using Lactose.Mapper;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pgvector.EntityFrameworkCore;
|
using Pgvector.EntityFrameworkCore;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository> logger) : IAssetRepository {
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Asset? Find(Guid id) => context.Assets.Find(id);
|
public Asset? Find(Guid id) => context.Assets.Find(id);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Asset? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) {
|
||||||
|
IQueryable<Asset> query = context.Assets
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Uploader)
|
||||||
|
.Where(a => a.Id == id);
|
||||||
|
|
||||||
|
return accessLevel switch {
|
||||||
|
>= EAccessLevel.Admin => query.FirstOrDefault(),
|
||||||
|
EAccessLevel.Curator => query.FirstOrDefault(a =>
|
||||||
|
a.DeletedAt == null || a.UploadedBy == userId),
|
||||||
|
EAccessLevel.Maintainer when userId.HasValue => query
|
||||||
|
.Include(a => a.Albums)
|
||||||
|
.FirstOrDefault(a => a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
a.Visibility == EVisibility.Protected ||
|
||||||
|
a.UploadedBy == userId ||
|
||||||
|
a.Albums!.Any(al => al.PersonOwnerId.HasValue &&
|
||||||
|
context.PersonMaintainers.Any(pm =>
|
||||||
|
pm.UserId == userId.Value && pm.PersonId == al.PersonOwnerId.Value))
|
||||||
|
)),
|
||||||
|
_ => query.FirstOrDefault(a =>
|
||||||
|
a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
(a.Visibility == EVisibility.Protected && userId.HasValue) ||
|
||||||
|
(a.Visibility == EVisibility.Private && userId.HasValue && a.UploadedBy == userId)
|
||||||
|
))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Asset? FindWithAlbums(Guid id) => context.Assets
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Albums)
|
||||||
|
.FirstOrDefault(a => a.Id == id);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to) =>
|
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to) =>
|
||||||
context.Assets.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
context.Assets.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
||||||
@@ -21,7 +60,7 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to, int pageNumber, int pageSize) =>
|
public IEnumerable<Asset> FindByDateRange(DateTime from, DateTime to, int pageNumber, int pageSize) =>
|
||||||
context.Assets.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to)
|
context.Assets.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip(pageNumber * pageSize)
|
||||||
.Take(pageSize);
|
.Take(pageSize);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -50,10 +89,13 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Asset> FindByOwner(Guid ownerId) => context.Assets.Where(a => a.Owner!.Id == ownerId);
|
public IEnumerable<Asset> FindByUploader(Guid uploaderId) => context.Assets.Where(a => a.UploadedBy == uploaderId);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids) => context.Assets.Where(a => ids.Contains(a.Id));
|
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids) => context.Assets
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Albums)
|
||||||
|
.Where(a => ids.Contains(a.Id));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Asset? FindByPath(string path) => context.Assets.FirstOrDefault(a => a.OriginalPath == path);
|
public Asset? FindByPath(string path) => context.Assets.FirstOrDefault(a => a.OriginalPath == path);
|
||||||
@@ -135,7 +177,7 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
.Take(limit);
|
.Take(limit);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<Asset> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total) {
|
public IEnumerable<Asset> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total, Guid? userId = null, EAccessLevel accessLevel = EAccessLevel.User, bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null) {
|
||||||
var query = context.Assets.AsQueryable();
|
var query = context.Assets.AsQueryable();
|
||||||
if (type.HasValue)
|
if (type.HasValue)
|
||||||
query = query.Where(a => a.Type == type.Value);
|
query = query.Where(a => a.Type == type.Value);
|
||||||
@@ -143,6 +185,33 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
query = query.Where(a => a.CreatedAt >= from.Value);
|
query = query.Where(a => a.CreatedAt >= from.Value);
|
||||||
if (to.HasValue)
|
if (to.HasValue)
|
||||||
query = query.Where(a => a.UpdatedAt <= to.Value);
|
query = query.Where(a => a.UpdatedAt <= to.Value);
|
||||||
|
if (unlinked)
|
||||||
|
query = query.Where(a => a.Albums!.Count == 0);
|
||||||
|
if (folderId.HasValue)
|
||||||
|
query = query.Where(a => a.FolderId == folderId.Value);
|
||||||
|
if (uploadedBy.HasValue)
|
||||||
|
query = query.Where(a => a.UploadedBy == uploadedBy.Value);
|
||||||
|
if (!string.IsNullOrEmpty(search))
|
||||||
|
query = query.Where(a => EF.Functions.ILike(a.OriginalFilename, $"%{search}%"));
|
||||||
|
|
||||||
|
// Apply visibility filter
|
||||||
|
query = accessLevel switch {
|
||||||
|
EAccessLevel.Admin => query,
|
||||||
|
EAccessLevel.Curator => query.Where(a => a.DeletedAt == null || a.UploadedBy == userId),
|
||||||
|
EAccessLevel.Maintainer => query.Where(a => a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
a.Visibility == EVisibility.Protected ||
|
||||||
|
a.UploadedBy == userId ||
|
||||||
|
a.Albums!.Any(al => al.PersonOwnerId.HasValue &&
|
||||||
|
context.PersonMaintainers.Any(pm =>
|
||||||
|
pm.UserId == userId && pm.PersonId == al.PersonOwnerId.Value))
|
||||||
|
)),
|
||||||
|
_ => query.Where(a => a.DeletedAt == null && (
|
||||||
|
a.Visibility == EVisibility.Public ||
|
||||||
|
(a.Visibility == EVisibility.Protected && userId.HasValue) ||
|
||||||
|
(a.Visibility == EVisibility.Private && userId.HasValue && a.UploadedBy == userId)
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
total = query.Count();
|
total = query.Count();
|
||||||
|
|
||||||
@@ -151,11 +220,12 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
var seedHash = seed.Value.GetHashCode();
|
var seedHash = seed.Value.GetHashCode();
|
||||||
var shuffledIds = allIds
|
var shuffledIds = allIds
|
||||||
.OrderBy(id => DeterministicHash(id, seedHash))
|
.OrderBy(id => DeterministicHash(id, seedHash))
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip(pageNumber * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var assets = context.Assets
|
var assets = context.Assets
|
||||||
|
.AsSplitQuery()
|
||||||
.Where(a => shuffledIds.Contains(a.Id))
|
.Where(a => shuffledIds.Contains(a.Id))
|
||||||
.Include(a => a.Albums!).ThenInclude(a => a.PersonOwner)
|
.Include(a => a.Albums!).ThenInclude(a => a.PersonOwner)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -163,11 +233,13 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
return shuffledIds.Select(id => assets.First(a => a.Id == id)).ToList();
|
return shuffledIds.Select(id => assets.First(a => a.Id == id)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
IQueryable<Asset> dataQuery = query.Include(a => a.Albums!).ThenInclude(a => a.PersonOwner);
|
IQueryable<Asset> dataQuery = query
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Albums!).ThenInclude(a => a.PersonOwner);
|
||||||
dataQuery = orderRandomly ? dataQuery.OrderBy(a => a.Id) : dataQuery.OrderByDescending(a => a.CreatedAt);
|
dataQuery = orderRandomly ? dataQuery.OrderBy(a => a.Id) : dataQuery.OrderByDescending(a => a.CreatedAt);
|
||||||
|
|
||||||
return dataQuery
|
return dataQuery
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip(pageNumber * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -193,4 +265,149 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
.Take(count)
|
.Take(count)
|
||||||
.Select(a => a.OriginalPath)
|
.Select(a => a.OriginalPath)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public List<Butter.Dtos.Asset.AssetGroupDto> GetFolderGroups(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly = true) {
|
||||||
|
logger.LogTrace("GetFolderGroups: userId={UserId}, accessLevel={AccessLevel}, unlinkedOnly={UnlinkedOnly}", userId, accessLevel, unlinkedOnly);
|
||||||
|
|
||||||
|
var query = context.Assets
|
||||||
|
.Where(a => a.DeletedAt == null);
|
||||||
|
|
||||||
|
if (unlinkedOnly)
|
||||||
|
query = query.Where(a => a.Albums!.Count == 0);
|
||||||
|
|
||||||
|
if (accessLevel < EAccessLevel.Curator) {
|
||||||
|
if (accessLevel == EAccessLevel.Maintainer && userId.HasValue) {
|
||||||
|
var count = query.Count(a => a.UploadedBy == userId);
|
||||||
|
return [new Butter.Dtos.Asset.AssetGroupDto {
|
||||||
|
FolderId = null,
|
||||||
|
Name = "My Uploads",
|
||||||
|
AssetCount = count
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = query
|
||||||
|
.GroupBy(a => a.FolderId)
|
||||||
|
.Select(g => new {
|
||||||
|
FolderId = g.Key,
|
||||||
|
AssetCount = g.Count()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var folderNames = context.Folders
|
||||||
|
.Where(f => groups.Select(g => g.FolderId).Contains(f.Id))
|
||||||
|
.ToDictionary(f => f.Id, f => f.BasePath);
|
||||||
|
|
||||||
|
var result = groups
|
||||||
|
.Where(g => g.AssetCount > 0)
|
||||||
|
.Select(g => new Butter.Dtos.Asset.AssetGroupDto {
|
||||||
|
FolderId = g.FolderId,
|
||||||
|
Name = g.FolderId.HasValue && folderNames.TryGetValue(g.FolderId.Value, out var name) ? name : "No Folder",
|
||||||
|
AssetCount = g.AssetCount
|
||||||
|
})
|
||||||
|
.OrderByDescending(g => g.AssetCount)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Butter.Dtos.Asset.AssetBrowseResultDto? BrowseAssets(Guid folderId, string currentPath, int page, int pageSize, Guid? userId, EAccessLevel accessLevel, string? search, bool unlinkedOnly = true) {
|
||||||
|
var folder = context.Folders.Find(folderId);
|
||||||
|
if (folder == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var basePath = folder.BasePath.TrimEnd('/') + "/";
|
||||||
|
var prefix = string.IsNullOrEmpty(currentPath) ? basePath : basePath + currentPath.TrimEnd('/') + "/";
|
||||||
|
var likePattern = prefix + "%";
|
||||||
|
|
||||||
|
var baseQuery = context.Assets
|
||||||
|
.Where(a => a.FolderId == folderId)
|
||||||
|
.Where(a => a.DeletedAt == null);
|
||||||
|
|
||||||
|
if (unlinkedOnly)
|
||||||
|
baseQuery = baseQuery.Where(a => a.Albums!.Count == 0);
|
||||||
|
|
||||||
|
baseQuery = accessLevel switch {
|
||||||
|
EAccessLevel.Admin => baseQuery,
|
||||||
|
EAccessLevel.Curator => baseQuery.Where(a => a.DeletedAt == null || a.UploadedBy == userId),
|
||||||
|
EAccessLevel.Maintainer => baseQuery.Where(a => a.UploadedBy == userId),
|
||||||
|
_ => baseQuery.Where(a => false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search))
|
||||||
|
baseQuery = baseQuery.Where(a => EF.Functions.ILike(a.OriginalFilename, $"%{search}%"));
|
||||||
|
|
||||||
|
var dirs = GetDirectoryNames(prefix, prefix.Length, folderId, userId, accessLevel, search, unlinkedOnly);
|
||||||
|
|
||||||
|
var levelQuery = baseQuery
|
||||||
|
.Where(a => EF.Functions.Like(a.OriginalPath, likePattern))
|
||||||
|
.Where(a => !EF.Functions.Like(a.OriginalPath, prefix + "%/%"));
|
||||||
|
|
||||||
|
var totalAtLevel = levelQuery.Count();
|
||||||
|
|
||||||
|
var assetEntities = levelQuery
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(a => a.Albums!).ThenInclude(al => al.PersonOwner)
|
||||||
|
.OrderBy(a => a.OriginalFilename)
|
||||||
|
.Skip(page * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var assetDtos = assetEntities.Select(a => a.ToAssetPreviewDto(accessLevel, userId)).ToList();
|
||||||
|
|
||||||
|
return new Butter.Dtos.Asset.AssetBrowseResultDto {
|
||||||
|
Directories = dirs,
|
||||||
|
Assets = assetDtos,
|
||||||
|
CurrentPath = currentPath,
|
||||||
|
TotalAssetCount = totalAtLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Butter.Dtos.Asset.DirectoryEntryDto> GetDirectoryNames(string prefix, int prefixLen, Guid folderId, Guid? userId, EAccessLevel accessLevel, string? search, bool unlinkedOnly) {
|
||||||
|
var sql = new System.Text.StringBuilder();
|
||||||
|
sql.Append("SELECT DISTINCT SPLIT_PART(SUBSTRING(a.\"OriginalPath\", LENGTH({0}) + 1), '/', 1) AS \"Name\" ");
|
||||||
|
sql.Append("FROM \"Assets\" a ");
|
||||||
|
if (unlinkedOnly)
|
||||||
|
sql.Append("LEFT JOIN \"AlbumAsset\" aa ON a.\"Id\" = aa.\"AssetsId\" ");
|
||||||
|
sql.Append("WHERE a.\"FolderId\" = {1} ");
|
||||||
|
sql.Append("AND a.\"DeletedAt\" IS NULL ");
|
||||||
|
if (unlinkedOnly)
|
||||||
|
sql.Append("AND aa.\"AssetsId\" IS NULL ");
|
||||||
|
sql.Append("AND a.\"OriginalPath\" LIKE {2} ");
|
||||||
|
sql.Append("AND a.\"OriginalPath\" LIKE {3} ");
|
||||||
|
sql.Append("AND LENGTH(a.\"OriginalPath\") > LENGTH({0}) ");
|
||||||
|
|
||||||
|
var parms = new List<object> { prefix, folderId, prefix + "%", prefix + "%/%" };
|
||||||
|
int p = 4;
|
||||||
|
|
||||||
|
switch (accessLevel) {
|
||||||
|
case EAccessLevel.Curator:
|
||||||
|
sql.Append($"AND (a.\"DeletedAt\" IS NULL OR a.\"UploadedBy\" = {{{p}}}) ");
|
||||||
|
parms.Add(userId ?? Guid.Empty);
|
||||||
|
p++;
|
||||||
|
break;
|
||||||
|
case EAccessLevel.Maintainer:
|
||||||
|
sql.Append($"AND a.\"UploadedBy\" = {{{p}}} ");
|
||||||
|
parms.Add(userId ?? Guid.Empty);
|
||||||
|
p++;
|
||||||
|
break;
|
||||||
|
case < EAccessLevel.Curator:
|
||||||
|
sql.Append("AND FALSE ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search)) {
|
||||||
|
sql.Append($"AND a.\"OriginalFilename\" ILIKE '%' || {{{p}}} || '%' ");
|
||||||
|
parms.Add(search!);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.Append("ORDER BY \"Name\"");
|
||||||
|
|
||||||
|
return context.Database
|
||||||
|
.SqlQueryRaw<Butter.Dtos.Asset.DirectoryEntryDto>(sql.ToString(), parms.ToArray())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Services;
|
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using Butter.Dtos;
|
||||||
|
using Butter.Dtos.Album;
|
||||||
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
@@ -37,11 +40,13 @@ public interface IAlbumRepository : IDisposable {
|
|||||||
public Album? Find(Guid id);
|
public Album? Find(Guid id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds albums by the owner's ID.
|
/// Finds an album by its ID with assets filtered by the requesting user's access level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ownerId">The ID of the owner.</param>
|
/// <param name="id">The ID of the album.</param>
|
||||||
/// <returns>A collection of albums owned by the specified owner.</returns>
|
/// <param name="userId">The requesting user's ID for visibility-scoped asset filtering.</param>
|
||||||
public IEnumerable<Album> FindByOwner(Guid ownerId);
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
|
/// <returns>The album if found; otherwise, null.</returns>
|
||||||
|
public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds albums by the person's ID.
|
/// Finds albums by the person's ID.
|
||||||
@@ -65,6 +70,14 @@ public interface IAlbumRepository : IDisposable {
|
|||||||
/// <returns>A collection of albums created or updated within the specified date range.</returns>
|
/// <returns>A collection of albums created or updated within the specified date range.</returns>
|
||||||
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to);
|
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges multiple source albums into a destination album.
|
||||||
|
/// All assets from source albums are moved to the destination, then source albums are hard-deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destId">The ID of the destination album.</param>
|
||||||
|
/// <param name="sourceIds">The IDs of the source albums to merge.</param>
|
||||||
|
public void MergeAlbums(Guid destId, List<Guid> sourceIds);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an album from the database.
|
/// Removes an album from the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,11 +85,18 @@ public interface IAlbumRepository : IDisposable {
|
|||||||
public void Remove(Album album);
|
public void Remove(Album album);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for albums based on a query string.
|
/// Searches for albums based on a query string with optional sorting, filtering, and access-level scoping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The search query string.</param>
|
/// <param name="query">The search query string to filter by title.</param>
|
||||||
/// <param name="page">The page number for pagination (default is 0).</param>
|
/// <param name="page">The page number for pagination (default is 0).</param>
|
||||||
/// <param name="pageSize">The number of results per page (default is 150).</param>
|
/// <param name="pageSize">The number of results per page (default is <see cref="PagedParametersDto.MaxPageSize"/>).</param>
|
||||||
/// <returns>A list of album IDs that match the search query.</returns>
|
/// <param name="sortBy">The field to sort by (e.g. "name", "created", "updated", "assets", "person"). When <c>null</c>, defaults to created descending.</param>
|
||||||
public IEnumerable<Album> SearchQuery(string query, int page = 0, int pageSize = 150);
|
/// <param name="sortAsc">Whether to sort ascending. Default is <c>false</c> (descending).</param>
|
||||||
}
|
/// <param name="unassigned">When <c>true</c>, only return albums with no person assigned.</param>
|
||||||
|
/// <param name="userId">The current user's ID for access-level filtering.</param>
|
||||||
|
/// <param name="accessLevel">The current user's access level. Regular users only see albums with visible assets.</param>
|
||||||
|
/// <param name="uploadedBy">Optional filter for albums containing assets uploaded by a specific user.</param>
|
||||||
|
/// <returns>A list of album previews matching the search query.</returns>
|
||||||
|
public IEnumerable<AlbumPreviewDto> SearchQuery(string query, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User, Guid? uploadedBy = null);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
@@ -15,6 +14,23 @@ public interface IAssetRepository : IDisposable {
|
|||||||
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
||||||
public Asset? Find(Guid id);
|
public Asset? Find(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an asset by its ID, respecting the requesting user's access level.
|
||||||
|
/// Returns null if the asset does not exist or is not visible to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the asset.</param>
|
||||||
|
/// <param name="userId">The requesting user's ID.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
|
/// <returns>The asset if found and visible; otherwise null.</returns>
|
||||||
|
public Asset? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an asset by its ID with its album navigation properties loaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the asset.</param>
|
||||||
|
/// <returns>Null if not found, otherwise the requested asset with albums.</returns>
|
||||||
|
public Asset? FindWithAlbums(Guid id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds a set of assets by their IDs.
|
/// Finds a set of assets by their IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -23,11 +39,11 @@ public interface IAssetRepository : IDisposable {
|
|||||||
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids);
|
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds all assets owned by the user.
|
/// Finds all assets uploaded by the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ownerId">The ID of the owner.</param>
|
/// <param name="uploaderId">The ID of the uploader.</param>
|
||||||
/// <returns>A collection of assets owned by the specified user.</returns>
|
/// <returns>A collection of assets uploaded by the specified user.</returns>
|
||||||
public IEnumerable<Asset> FindByOwner(Guid ownerId);
|
public IEnumerable<Asset> FindByUploader(Guid uploaderId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds all assets created or updated between the given dates.
|
/// Finds all assets created or updated between the given dates.
|
||||||
@@ -161,16 +177,48 @@ public interface IAssetRepository : IDisposable {
|
|||||||
List<string> GetRandomPathsByFolder(Guid folderId, int count);
|
List<string> GetRandomPathsByFolder(Guid folderId, int count);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queries assets with optional type filter, date range, and random ordering.
|
/// Queries assets with optional type filter, date range, random ordering, and visibility scoping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">Optional asset type to filter by.</param>
|
/// <param name="type">Optional asset type to filter by.</param>
|
||||||
/// <param name="from">Optional start date for the date range filter.</param>
|
/// <param name="from">Optional start date for the date range filter.</param>
|
||||||
/// <param name="to">Optional end date for the date range filter.</param>
|
/// <param name="to">Optional end date for the date range filter.</param>
|
||||||
/// <param name="orderRandomly">If true, orders randomly instead of by date.</param>
|
/// <param name="orderRandomly">If true, orders randomly instead of by date.</param>
|
||||||
/// <param name="seed">An optional seed for deterministic random ordering across paginated requests.</param>
|
/// <param name="seed">An optional seed for deterministic random ordering across paginated requests.</param>
|
||||||
/// <param name="pageNumber">The 1-based page number.</param>
|
/// <param name="pageNumber">The zero-based page number.</param>
|
||||||
/// <param name="pageSize">The number of items per page.</param>
|
/// <param name="pageSize">The number of items per page.</param>
|
||||||
/// <param name="total">The total count of matching assets.</param>
|
/// <param name="total">The total count of matching assets (after visibility filtering).</param>
|
||||||
|
/// <param name="userId">The requesting user's ID for visibility filtering.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level for visibility filtering.</param>
|
||||||
|
/// <param name="unlinked">If true, filters for assets not assigned to any album.</param>
|
||||||
|
/// <param name="folderId">Optional folder ID to filter assets by their scan folder.</param>
|
||||||
|
/// <param name="uploadedBy">Optional uploader user ID to filter assets by their uploader.</param>
|
||||||
|
/// <param name="search">Optional search term for ILike matching against OriginalFilename.</param>
|
||||||
/// <returns>A paginated collection of assets matching the filters.</returns>
|
/// <returns>A paginated collection of assets matching the filters.</returns>
|
||||||
IEnumerable<Asset> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total);
|
IEnumerable<Asset> GetAssets(EAssetType? type, DateTime? from, DateTime? to, bool orderRandomly, Guid? seed, int pageNumber, int pageSize, out int total, Guid? userId = null, EAccessLevel accessLevel = EAccessLevel.User, bool unlinked = false, Guid? folderId = null, Guid? uploadedBy = null, string? search = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns groups of unlinked assets (not assigned to any album), grouped by folder for drill-down browsing.
|
||||||
|
/// Admins and curators receive all folders with their unlinked asset counts. Maintainers receive a single
|
||||||
|
/// "My Uploads" group with their own unlinked upload count.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The requesting user's ID.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
|
/// <param name="unlinkedOnly">If true (default), only counts assets not assigned to any album.</param>
|
||||||
|
/// <returns>A list of unlinked asset groups with folder ID, name, and asset count.</returns>
|
||||||
|
List<Butter.Dtos.Asset.AssetGroupDto> GetFolderGroups(Guid? userId, EAccessLevel accessLevel, bool unlinkedOnly = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Browses assets at a specific directory level within a folder, returning immediate subdirectories
|
||||||
|
/// and the assets located directly at that level with pagination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The root folder ID to browse within.</param>
|
||||||
|
/// <param name="currentPath">The current path relative to the folder root (empty string for root).</param>
|
||||||
|
/// <param name="page">The zero-based page number for asset pagination.</param>
|
||||||
|
/// <param name="pageSize">The number of assets per page.</param>
|
||||||
|
/// <param name="userId">The requesting user's ID for visibility filtering.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
|
/// <param name="search">Optional search term for ILike matching against OriginalFilename.</param>
|
||||||
|
/// <param name="unlinkedOnly">If true (default), only includes assets not assigned to any album.</param>
|
||||||
|
/// <returns>A browse result containing subdirectories and a page of assets at the current level.</returns>
|
||||||
|
Butter.Dtos.Asset.AssetBrowseResultDto? BrowseAssets(Guid folderId, string currentPath, int page, int pageSize, Guid? userId, EAccessLevel accessLevel, string? search, bool unlinkedOnly = true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public interface IJobRecordRepository : IDisposable {
|
|||||||
/// <param name="parentId">The parent job ID.</param>
|
/// <param name="parentId">The parent job ID.</param>
|
||||||
List<JobRecord> GetChildren(Guid parentId);
|
List<JobRecord> GetChildren(Guid parentId);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Retrieves child job records for a given parent job that have been modified since the specified timestamp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentId">The parent job ID.</param>
|
||||||
|
/// <param name="since">Only return records modified after this timestamp.</param>
|
||||||
|
List<JobRecord> GetChildrenModifiedSince(Guid parentId, DateTime since);
|
||||||
|
/// <summary>
|
||||||
/// Retrieves a job record by its ID.
|
/// Retrieves a job record by its ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The job record ID.</param>
|
/// <param name="id">The job record ID.</param>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter.Dtos;
|
using Butter.Dtos;
|
||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
@@ -11,22 +12,25 @@ public interface IMediaRepository {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The ID of the media.</param>
|
/// <param name="id">The ID of the media.</param>
|
||||||
/// <param name="userId">The ID of the user requesting the media.</param>
|
/// <param name="userId">The ID of the user requesting the media.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
||||||
public MediaDto? GetOriginalData(Guid id, Guid? userId);
|
public MediaDto? GetOriginalData(Guid id, Guid? userId, EAccessLevel accessLevel = EAccessLevel.User);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the thumbnail media data.
|
/// Retrieves the thumbnail media data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The ID of the media.</param>
|
/// <param name="id">The ID of the media.</param>
|
||||||
/// <param name="userId">The ID of the user requesting the media.</param>
|
/// <param name="userId">The ID of the user requesting the media.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
||||||
public MediaDto? GetThumbData(Guid id, Guid? userId);
|
public MediaDto? GetThumbData(Guid id, Guid? userId, EAccessLevel accessLevel = EAccessLevel.User);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the preview media data.
|
/// Retrieves the preview media data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The ID of the media.</param>
|
/// <param name="id">The ID of the media.</param>
|
||||||
/// <param name="userId">The ID of the user requesting the media.</param>
|
/// <param name="userId">The ID of the user requesting the media.</param>
|
||||||
|
/// <param name="accessLevel">The requesting user's access level.</param>
|
||||||
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
/// <returns>A <see cref="MediaDto"/> containing the media data, or null if not found or unauthorized.</returns>
|
||||||
public MediaDto? GetPreviewData(Guid id, Guid? userId);
|
public MediaDto? GetPreviewData(Guid id, Guid? userId, EAccessLevel accessLevel = EAccessLevel.User);
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user