Compare commits
225 Commits
refactor/c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
b851b9f6e2
|
|||
|
c58cbf8de9
|
|||
|
fc2215ec63
|
|||
|
a3c1d74501
|
|||
|
b7aac30384
|
|||
|
f217c0fb30
|
|||
|
c3304e0663
|
|||
|
ca21acbff6
|
|||
|
6488e2224e
|
|||
|
fa81a0bbbb
|
|||
|
9513a460d0
|
|||
|
68d0881e34
|
|||
|
2daf8f5d77
|
|||
|
10e680ed07
|
|||
|
0762eec540
|
|||
|
db5dcf19b2
|
|||
|
988d9695b3
|
|||
|
2401eed3ed
|
|||
|
0e1f13ae7d
|
|||
|
77440bd4b9
|
|||
|
625d4e1a13
|
|||
|
a4c2892a66
|
|||
|
ab70816a07
|
|||
|
065b86403a
|
|||
|
fc1edf0ea3
|
|||
|
5a99665e4e
|
|||
|
28a13f7baf
|
|||
|
1173f98ec6
|
|||
|
979e0cb3f2
|
|||
|
69736f0850
|
|||
|
39bf967186
|
|||
|
5fdaa2c7f8
|
|||
|
03010b9151
|
|||
|
1fb4b61e51
|
|||
|
8e39004f68
|
|||
|
4b7740e606
|
|||
|
0f74ed61fd
|
|||
|
2d1e43b02e
|
|||
|
dadf3c67bf
|
|||
|
2e945ee477
|
|||
|
0feb66e341
|
|||
|
87d9267285
|
|||
|
c11bf579c4
|
|||
|
7085f43e54
|
|||
|
ed7d54c47a
|
|||
|
bd41568578
|
|||
|
c052f17623
|
|||
|
6d5303f99c
|
|||
|
de1175bdc7
|
|||
|
b03e9bea5e
|
|||
|
f3779cc788
|
|||
|
1aff1d7731
|
|||
|
98c100c864
|
|||
|
8177bda232
|
|||
|
cb04e53b7e
|
|||
|
c16add5dfe
|
|||
|
1fc9c68d80
|
|||
|
cf736be61a
|
|||
|
cc7992ead9
|
|||
|
7a0ba166dc
|
|||
|
0cc6f86f3b
|
|||
|
6b592156c9
|
|||
|
6fd77c5c31
|
|||
|
94d91ec8b2
|
|||
|
82b517ed2c
|
|||
|
1596897a5b
|
|||
|
9ecc64352c
|
|||
|
9c75394aa6
|
|||
|
d036443a36
|
|||
|
6c4358a4ce
|
|||
|
eeb583d78d
|
|||
|
c588b6f234
|
|||
|
306934304e
|
|||
|
24b1f24dea
|
|||
|
913a6e7382
|
|||
|
b90d1be552
|
|||
|
07b8c99682
|
|||
|
ede49333f8
|
|||
|
c4b2b2f61f
|
|||
|
501fce894e
|
|||
|
aa85a28d04
|
|||
|
c515ddff51
|
|||
|
b8686bd7e3
|
|||
|
50a3c2d038
|
|||
|
683fbf1a68
|
|||
|
b3633538cd
|
|||
|
6212820d74
|
|||
|
ab37bbc7b0
|
|||
|
c503083df7
|
|||
|
466a52ecd9
|
|||
|
c28d7bedd7
|
|||
|
8c19bd6a73
|
|||
|
7c5c92a501
|
|||
|
fb15930611
|
|||
|
a72dbcfc0c
|
|||
|
923ec0a157
|
|||
|
a0103ada64
|
|||
|
de3aa21909
|
|||
|
247296476c
|
|||
|
a795ff6db8
|
|||
|
d07e33cb75
|
|||
|
78f1d0ecd3
|
|||
|
14d5254461
|
|||
|
c59fc011f4
|
|||
|
3e59a102af
|
|||
|
42b46243a4
|
|||
|
3c83fdfc4d
|
|||
|
62a8153479
|
|||
|
0ec49787fb
|
|||
|
f49f17a9db
|
|||
|
c11b30d0bb
|
|||
|
add9fa49e5
|
|||
|
4815d31b31
|
|||
|
91764593c7
|
|||
|
ebd7539c95
|
|||
|
6c8c52e3b2
|
|||
|
413ae80c96
|
|||
|
3da6de1feb
|
|||
|
caf5468dad
|
|||
|
2b6cf503a5
|
|||
|
7991f88df5
|
|||
|
a005cfb143
|
|||
|
a11544c056
|
|||
|
cd8e6714b2
|
|||
|
eb8d126261
|
|||
|
71031e2222
|
|||
|
cb37edc0bb
|
|||
|
b9230699c5
|
|||
|
f2856c10a3
|
|||
|
0519f2a2e6
|
|||
|
6aa6833163
|
|||
|
8dc01c8a85
|
|||
|
67cd372b8d
|
|||
|
cfb4428e78
|
|||
|
1d95d637dd
|
|||
|
f42fc1da1c
|
|||
|
30cbbf0139
|
|||
|
d02edbd38d
|
|||
|
10067f6141
|
|||
|
6a360fe697
|
|||
|
777c0c089a
|
|||
|
6fdf34787d
|
|||
|
72b0739f41
|
|||
|
f556313f1d
|
|||
|
7fd75395f8
|
|||
|
70260967be
|
|||
|
db94b21aef
|
|||
|
d8d94d0aec
|
|||
|
e7bf760888
|
|||
|
7f5b447b3c
|
|||
|
84da11f301
|
|||
|
05a02046a9
|
|||
|
ce20c5980b
|
|||
|
bb71c558b1
|
|||
|
b76f614975
|
|||
|
fc89b46f98
|
|||
|
39587ed346
|
|||
|
f83327474e
|
|||
|
0961325642
|
|||
|
a63d21ed06
|
|||
|
7b09e63918
|
|||
|
cda48ea18d
|
|||
|
7cb471e978
|
|||
|
44a791db1f
|
|||
|
6cba70ee12
|
|||
|
ceadb5ad9b
|
|||
|
2e8a1d05a1
|
|||
|
df077b347e
|
|||
|
21108c19a9
|
|||
|
95472df02b
|
|||
|
9f4a7a3fe8
|
|||
|
2471fa2e75
|
|||
|
f06d93a348
|
|||
|
983f57c4c2
|
|||
|
00cd7ad2d8
|
|||
|
2bffbf18a3
|
|||
|
07445ebc25
|
|||
|
f83fb5d8a9
|
|||
|
a3e13d1581
|
|||
|
677d9761f9
|
|||
|
23c435e036
|
|||
|
fc61235d0c
|
|||
|
6e1b67609a
|
|||
|
4be054163b
|
|||
|
009f66154c
|
|||
|
5d3bd1144d
|
|||
|
334fa9b9a7
|
|||
|
bb5d70eddb
|
|||
|
b51a086031
|
|||
|
27afe5da9f
|
|||
|
9d1bc46bf1
|
|||
|
be176ef0c2
|
|||
|
93e7b04e74
|
|||
|
d9f10fd598
|
|||
|
50518351bc
|
|||
|
4443da5660
|
|||
|
b193224a2c
|
|||
|
4e9c5733d1
|
|||
|
1c6b324b0d
|
|||
|
ded3a70cb7
|
|||
|
9e54b61eee
|
|||
|
43d89299c3
|
|||
|
1af11b2a99
|
|||
|
1a31d7cbe7
|
|||
|
f0d6772dca
|
|||
|
24836fc606
|
|||
|
0bc77b948c
|
|||
|
f792d43ab9
|
|||
|
1b45be225a
|
|||
|
7811545726
|
|||
|
213608d4f0
|
|||
|
bca6a2ffde
|
|||
|
885b895a3a
|
|||
|
08941a282b
|
|||
|
4fd455acbf
|
|||
|
5ff1539f18
|
|||
|
3c023a71b1
|
|||
|
49d8eaa7b2
|
|||
|
16a37549fe
|
|||
|
2aff62c64f
|
|||
|
a49d485943
|
|||
|
4c65602465
|
|||
|
4242953969
|
|||
|
c9530ac8b5
|
|||
|
4ba7d38d78
|
43
.env.testing.example
Normal file
43
.env.testing.example
Normal file
@@ -0,0 +1,43 @@
|
||||
# ActivityPub Testing Environment Variables
|
||||
|
||||
# Solar Network Configuration
|
||||
SOLAR_DOMAIN=solar.local
|
||||
SOLAR_PORT=5000
|
||||
SOLAR_URL=http://solar.local:5000
|
||||
|
||||
# Mastodon (Self-Hosted Test Instance)
|
||||
MASTODON_DOMAIN=mastodon.local
|
||||
MASTODON_PORT=3001
|
||||
MASTODON_STREAMING_PORT=4000
|
||||
MASTODON_URL=http://mastodon.local:3001
|
||||
|
||||
# Database
|
||||
DB_CONNECTION_STRING=Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres
|
||||
|
||||
# Test Accounts
|
||||
SOLAR_TEST_USERNAME=solaruser
|
||||
MASTODON_TEST_USERNAME=testuser
|
||||
MASTODON_TEST_PASSWORD=TestPassword123!
|
||||
|
||||
# ActivityPub Settings
|
||||
ACTIVITYPUB_DOMAIN=solar.local
|
||||
ACTIVITYPUB_ENABLE_FEDERATION=true
|
||||
ACTIVITYPUB_SIGNATURE_ALGORITHM=rsa-sha256
|
||||
|
||||
# HTTP Settings
|
||||
HTTP_TIMEOUT=30
|
||||
HTTP_MAX_RETRIES=3
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=Debug
|
||||
ACTIVITYPUB_LOG_LEVEL=Trace
|
||||
|
||||
# Testing
|
||||
TEST_SKIP_DATABASE_RESET=false
|
||||
TEST_SKIP_MASTODON_SETUP=false
|
||||
TEST_AUTO_ACCEPT_FOLLOWS=false
|
||||
|
||||
# Development (only in dev environment)
|
||||
DEV_DISABLE_SIGNATURE_VERIFICATION=false
|
||||
DEV_LOG_HTTP_BODIES=false
|
||||
DEV_DISABLE_CORS=false
|
||||
176
.github/workflows/docker-build.yml
vendored
176
.github/workflows/docker-build.yml
vendored
@@ -1,103 +1,103 @@
|
||||
name: Build and Push Microservices
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
determine-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.changes.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
determine-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.changes.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine changed services
|
||||
id: changes
|
||||
run: |
|
||||
files="${{ steps.changed-files.outputs.files }}"
|
||||
matrix="{\"include\":[]}"
|
||||
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone")
|
||||
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone")
|
||||
changed_services=()
|
||||
- name: Determine changed services
|
||||
id: changes
|
||||
run: |
|
||||
files="${{ steps.changed-files.outputs.files }}"
|
||||
matrix="{\"include\":[]}"
|
||||
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone" "Messager")
|
||||
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone" "messager")
|
||||
changed_services=()
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == DysonNetwork.Shared/* ]]; then
|
||||
changed_services=("${services[@]}")
|
||||
break
|
||||
fi
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then
|
||||
# check if service is already in changed_services
|
||||
if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then
|
||||
changed_services+=("${services[$i]}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
for file in $files; do
|
||||
if [[ "$file" == DysonNetwork.Shared/* ]]; then
|
||||
changed_services=("${services[@]}")
|
||||
break
|
||||
fi
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then
|
||||
# check if service is already in changed_services
|
||||
if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then
|
||||
changed_services+=("${services[$i]}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ ${#changed_services[@]} -gt 0 ]; then
|
||||
json_objects=""
|
||||
for service in "${changed_services[@]}"; do
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "${services[$i]}" == "$service" ]]; then
|
||||
image="${images[$i]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
json_objects+="{\"service\":\"$service\",\"image\":\"$image\"},"
|
||||
done
|
||||
matrix="{\"include\":[${json_objects%,}]}"
|
||||
fi
|
||||
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
||||
if [ ${#changed_services[@]} -gt 0 ]; then
|
||||
json_objects=""
|
||||
for service in "${changed_services[@]}"; do
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "${services[$i]}" == "$service" ]]; then
|
||||
image="${images[$i]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
json_objects+="{\"service\":\"$service\",\"image\":\"$image\"},"
|
||||
done
|
||||
matrix="{\"include\":[${json_objects%,}]}"
|
||||
fi
|
||||
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
||||
|
||||
build-and-push:
|
||||
needs: determine-changes
|
||||
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
|
||||
build-and-push:
|
||||
needs: determine-changes
|
||||
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image for ${{ matrix.service }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: DysonNetwork.${{ matrix.service }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
|
||||
platforms: linux/amd64
|
||||
- name: Build and push Docker image for ${{ matrix.service }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: DysonNetwork.${{ matrix.service }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
@@ -1,613 +0,0 @@
|
||||
# Wallet Funds API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Wallet Funds API provides red packet functionality for the DysonNetwork platform, allowing users to create and distribute funds among multiple recipients with expiration and claiming mechanisms.
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require Bearer token authentication:
|
||||
|
||||
```
|
||||
Authorization: Bearer {jwt_token}
|
||||
```
|
||||
|
||||
## Data Types
|
||||
|
||||
### Enums
|
||||
|
||||
#### FundSplitType
|
||||
```typescript
|
||||
enum FundSplitType {
|
||||
Even = 0, // Equal distribution
|
||||
Random = 1 // Lucky draw distribution
|
||||
}
|
||||
```
|
||||
|
||||
#### FundStatus
|
||||
```typescript
|
||||
enum FundStatus {
|
||||
Created = 0, // Fund created, waiting for claims
|
||||
PartiallyReceived = 1, // Some recipients claimed
|
||||
FullyReceived = 2, // All recipients claimed
|
||||
Expired = 3, // Fund expired, unclaimed amounts refunded
|
||||
Refunded = 4 // Legacy status
|
||||
}
|
||||
```
|
||||
|
||||
### Request/Response Models
|
||||
|
||||
#### CreateFundRequest
|
||||
```typescript
|
||||
interface CreateFundRequest {
|
||||
recipientAccountIds: string[]; // UUIDs of recipients
|
||||
currency: string; // e.g., "points", "golds"
|
||||
totalAmount: number; // Total amount to distribute
|
||||
splitType: FundSplitType; // Even or Random
|
||||
message?: string; // Optional message
|
||||
expirationHours?: number; // Optional: hours until expiration (default: 24)
|
||||
pinCode: string; // Required: 6-digit PIN code for security
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletFund
|
||||
```typescript
|
||||
interface SnWalletFund {
|
||||
id: string; // UUID
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
splitType: FundSplitType;
|
||||
status: FundStatus;
|
||||
message?: string;
|
||||
creatorAccountId: string; // UUID
|
||||
creatorAccount: SnAccount; // Creator account details (includes profile)
|
||||
recipients: SnWalletFundRecipient[];
|
||||
expiredAt: string; // ISO 8601 timestamp
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletFundRecipient
|
||||
```typescript
|
||||
interface SnWalletFundRecipient {
|
||||
id: string; // UUID
|
||||
fundId: string; // UUID
|
||||
recipientAccountId: string; // UUID
|
||||
recipientAccount: SnAccount; // Recipient account details (includes profile)
|
||||
amount: number; // Allocated amount
|
||||
isReceived: boolean;
|
||||
receivedAt?: string; // ISO 8601 timestamp (if claimed)
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletTransaction
|
||||
```typescript
|
||||
interface SnWalletTransaction {
|
||||
id: string; // UUID
|
||||
payerWalletId?: string; // UUID (null for system transfers)
|
||||
payeeWalletId?: string; // UUID (null for system transfers)
|
||||
currency: string;
|
||||
amount: number;
|
||||
remarks?: string;
|
||||
type: TransactionType;
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response
|
||||
```typescript
|
||||
interface ErrorResponse {
|
||||
type: string; // Error type
|
||||
title: string; // Error title
|
||||
status: number; // HTTP status code
|
||||
detail: string; // Error details
|
||||
instance?: string; // Request instance
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Create Fund
|
||||
|
||||
Creates a new fund (red packet) for distribution among recipients.
|
||||
|
||||
**Endpoint:** `POST /api/wallets/funds`
|
||||
|
||||
**Request Body:** `CreateFundRequest`
|
||||
|
||||
**Response:** `SnWalletFund` (201 Created)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST "/api/wallets/funds" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"recipientAccountIds": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
"550e8400-e29b-41d4-a716-446655440002"
|
||||
],
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": "Even",
|
||||
"message": "Happy New Year! 🎉",
|
||||
"expirationHours": 48,
|
||||
"pinCode": "123456"
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": 0,
|
||||
"status": 0,
|
||||
"message": "Happy New Year! 🎉",
|
||||
"creatorAccountId": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"creatorAccount": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"username": "creator_user"
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440005",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"amount": 33.34,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440006",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"amount": 33.33,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440007",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"amount": 33.33,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
],
|
||||
"expiredAt": "2025-10-05T22:00:00Z",
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Invalid parameters, insufficient funds, invalid recipients
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `403 Forbidden`: Invalid PIN code
|
||||
- `422 Unprocessable Entity`: Business logic violations
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Funds
|
||||
|
||||
Retrieves funds that the authenticated user is involved in (as creator or recipient).
|
||||
|
||||
**Endpoint:** `GET /api/wallets/funds`
|
||||
|
||||
**Query Parameters:**
|
||||
- `offset` (number, optional): Pagination offset (default: 0)
|
||||
- `take` (number, optional): Number of items to return (default: 20, max: 100)
|
||||
- `status` (FundStatus, optional): Filter by fund status
|
||||
|
||||
**Response:** `SnWalletFund[]` (200 OK)
|
||||
|
||||
**Headers:**
|
||||
- `X-Total`: Total number of funds matching the criteria
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/funds?offset=0&take=10&status=0" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": 0,
|
||||
"status": 0,
|
||||
"message": "Happy New Year! 🎉",
|
||||
"creatorAccountId": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"creatorAccount": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"username": "creator_user"
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440005",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"amount": 33.34,
|
||||
"isReceived": false
|
||||
}
|
||||
],
|
||||
"expiredAt": "2025-10-05T22:00:00Z",
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
|
||||
---
|
||||
|
||||
### 3. Get Fund
|
||||
|
||||
Retrieves details of a specific fund.
|
||||
|
||||
**Endpoint:** `GET /api/wallets/funds/{id}`
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Fund UUID
|
||||
|
||||
**Response:** `SnWalletFund` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:** (Same as create fund response)
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `403 Forbidden`: User doesn't have permission to view this fund
|
||||
- `404 Not Found`: Fund not found
|
||||
|
||||
---
|
||||
|
||||
### 4. Receive Fund
|
||||
|
||||
Claims the authenticated user's portion of a fund.
|
||||
|
||||
**Endpoint:** `POST /api/wallets/funds/{id}/receive`
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Fund UUID
|
||||
|
||||
**Response:** `SnWalletTransaction` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440008",
|
||||
"payerWalletId": null,
|
||||
"payeeWalletId": "550e8400-e29b-41d4-a716-446655440009",
|
||||
"currency": "points",
|
||||
"amount": 33.34,
|
||||
"remarks": "Received fund portion from 550e8400-e29b-41d4-a716-446655440004",
|
||||
"type": 1,
|
||||
"createdAt": "2025-10-03T22:05:00Z",
|
||||
"updatedAt": "2025-10-03T22:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Fund expired, already claimed, not a recipient
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `404 Not Found`: Fund not found
|
||||
|
||||
---
|
||||
|
||||
### 5. Get Wallet Overview
|
||||
|
||||
Retrieves a summarized overview of wallet transactions grouped by type for graphing/charting purposes.
|
||||
|
||||
**Endpoint:** `GET /api/wallets/overview`
|
||||
|
||||
**Query Parameters:**
|
||||
- `startDate` (string, optional): Start date in ISO 8601 format (e.g., "2025-01-01T00:00:00Z")
|
||||
- `endDate` (string, optional): End date in ISO 8601 format (e.g., "2025-12-31T23:59:59Z")
|
||||
|
||||
**Response:** `WalletOverview` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/overview?startDate=2025-01-01T00:00:00Z&endDate=2025-12-31T23:59:59Z" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"accountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"startDate": "2025-01-01T00:00:00.0000000Z",
|
||||
"endDate": "2025-12-31T23:59:59.0000000Z",
|
||||
"summary": {
|
||||
"System": {
|
||||
"type": "System",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 150.00,
|
||||
"spending": 0.00,
|
||||
"net": 150.00
|
||||
}
|
||||
}
|
||||
},
|
||||
"Transfer": {
|
||||
"type": "Transfer",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 25.00,
|
||||
"spending": 75.00,
|
||||
"net": -50.00
|
||||
},
|
||||
"golds": {
|
||||
"currency": "golds",
|
||||
"income": 0.00,
|
||||
"spending": 10.00,
|
||||
"net": -10.00
|
||||
}
|
||||
}
|
||||
},
|
||||
"Order": {
|
||||
"type": "Order",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 0.00,
|
||||
"spending": 200.00,
|
||||
"net": -200.00
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalIncome": 175.00,
|
||||
"totalSpending": 285.00,
|
||||
"netTotal": -110.00
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `accountId`: User's account UUID
|
||||
- `startDate`/`endDate`: Date range applied (ISO 8601 format)
|
||||
- `summary`: Object keyed by transaction type
|
||||
- `type`: Transaction type name
|
||||
- `currencies`: Object keyed by currency code
|
||||
- `currency`: Currency name
|
||||
- `income`: Total money received
|
||||
- `spending`: Total money spent
|
||||
- `net`: Income minus spending
|
||||
- `totalIncome`: Sum of all income across all types/currencies
|
||||
- `totalSpending`: Sum of all spending across all types/currencies
|
||||
- `netTotal`: Overall net (totalIncome - totalSpending)
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
|
||||
## Error Codes
|
||||
|
||||
### Common Error Types
|
||||
|
||||
#### Validation Errors
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "At least one recipient is required",
|
||||
"instance": "/api/wallets/funds"
|
||||
}
|
||||
```
|
||||
|
||||
#### Insufficient Funds
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Insufficient funds",
|
||||
"instance": "/api/wallets/funds"
|
||||
}
|
||||
```
|
||||
|
||||
#### Fund Not Available
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Fund is no longer available",
|
||||
"instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive"
|
||||
}
|
||||
```
|
||||
|
||||
#### Already Claimed
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "You have already received this fund",
|
||||
"instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive"
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- **Create Fund**: 10 requests per minute per user
|
||||
- **Get Funds**: 60 requests per minute per user
|
||||
- **Get Fund**: 60 requests per minute per user
|
||||
- **Receive Fund**: 30 requests per minute per user
|
||||
|
||||
## Webhooks/Notifications
|
||||
|
||||
The system integrates with the platform's notification system:
|
||||
|
||||
- **Fund Created**: Creator receives confirmation
|
||||
- **Fund Claimed**: Creator receives notification when someone claims
|
||||
- **Fund Expired**: Creator receives refund notification
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```typescript
|
||||
// Create a fund
|
||||
const createFund = async (fundData: CreateFundRequest): Promise<SnWalletFund> => {
|
||||
const response = await fetch('/api/wallets/funds', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(fundData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Get user's funds
|
||||
const getFunds = async (params?: {
|
||||
offset?: number;
|
||||
take?: number;
|
||||
status?: FundStatus;
|
||||
}): Promise<SnWalletFund[]> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.offset) queryParams.set('offset', params.offset.toString());
|
||||
if (params?.take) queryParams.set('take', params.take.toString());
|
||||
if (params?.status !== undefined) queryParams.set('status', params.status.toString());
|
||||
|
||||
const response = await fetch(`/api/wallets/funds?${queryParams}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Claim a fund
|
||||
const receiveFund = async (fundId: string): Promise<SnWalletTransaction> => {
|
||||
const response = await fetch(`/api/wallets/funds/${fundId}/receive`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
|
||||
class FundSplitType(Enum):
|
||||
EVEN = 0
|
||||
RANDOM = 1
|
||||
|
||||
class FundStatus(Enum):
|
||||
CREATED = 0
|
||||
PARTIALLY_RECEIVED = 1
|
||||
FULLY_RECEIVED = 2
|
||||
EXPIRED = 3
|
||||
REFUNDED = 4
|
||||
|
||||
def create_fund(token: str, fund_data: dict) -> dict:
|
||||
"""Create a new fund"""
|
||||
response = requests.post(
|
||||
'/api/wallets/funds',
|
||||
json=fund_data,
|
||||
headers={
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_funds(
|
||||
token: str,
|
||||
offset: int = 0,
|
||||
take: int = 20,
|
||||
status: Optional[FundStatus] = None
|
||||
) -> List[dict]:
|
||||
"""Get user's funds"""
|
||||
params = {'offset': offset, 'take': take}
|
||||
if status is not None:
|
||||
params['status'] = status.value
|
||||
|
||||
response = requests.get(
|
||||
'/api/wallets/funds',
|
||||
params=params,
|
||||
headers={'Authorization': f'Bearer {token}'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def receive_fund(token: str, fund_id: str) -> dict:
|
||||
"""Claim a fund portion"""
|
||||
response = requests.post(
|
||||
f'/api/wallets/funds/{fund_id}/receive',
|
||||
headers={'Authorization': f'Bearer {token}'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0
|
||||
- Initial release with basic red packet functionality
|
||||
- Support for even and random split types
|
||||
- 24-hour expiration with automatic refunds
|
||||
- RESTful API endpoints
|
||||
- Comprehensive error handling
|
||||
|
||||
## Support
|
||||
|
||||
For API support or questions:
|
||||
- Check the main documentation at `README_WALLET_FUNDS.md`
|
||||
- Review error messages for specific guidance
|
||||
- Contact the development team for technical issues
|
||||
@@ -4,8 +4,8 @@ var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var isDev = builder.Environment.IsDevelopment();
|
||||
|
||||
var cache = builder.AddRedis("cache");
|
||||
var queue = builder.AddNats("queue").WithJetStream();
|
||||
var cache = builder.AddRedis("Cache");
|
||||
var queue = builder.AddNats("Queue").WithJetStream();
|
||||
|
||||
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring");
|
||||
var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
|
||||
@@ -32,11 +32,26 @@ var zoneService = builder.AddProject<Projects.DysonNetwork_Zone>("zone")
|
||||
.WithReference(sphereService)
|
||||
.WithReference(developService)
|
||||
.WithReference(insightService);
|
||||
var messagerService = builder.AddProject<Projects.DysonNetwork_Messager>("messager")
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithReference(sphereService)
|
||||
.WithReference(developService)
|
||||
.WithReference(driveService);
|
||||
|
||||
passService.WithReference(developService).WithReference(driveService);
|
||||
|
||||
List<IResourceBuilder<ProjectResource>> services =
|
||||
[ringService, passService, driveService, sphereService, developService, insightService, zoneService];
|
||||
[
|
||||
ringService,
|
||||
passService,
|
||||
driveService,
|
||||
sphereService,
|
||||
developService,
|
||||
insightService,
|
||||
zoneService,
|
||||
messagerService
|
||||
];
|
||||
|
||||
for (var idx = 0; idx < services.Count; idx++)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0"/>
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -11,10 +11,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.0.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.0" />
|
||||
<PackageReference Include="Aspire.Hosting.Docker" Version="13.0.0-preview.1.25560.3"/>
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="13.0.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="13.1.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="13.1.0"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj"/>
|
||||
@@ -25,5 +25,6 @@
|
||||
<ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj"/>
|
||||
<ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj"/>
|
||||
<ProjectReference Include="..\DysonNetwork.Zone\DysonNetwork.Zone.csproj"/>
|
||||
<ProjectReference Include="..\DysonNetwork.Messager\DysonNetwork.Messager.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -18,6 +18,7 @@ public class AppDatabase(
|
||||
public DbSet<SnCustomApp> CustomApps { get; set; } = null!;
|
||||
public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||
public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
|
||||
public DbSet<SnMiniApp> MiniApps { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libkrb5-3 \
|
||||
libgssapi-krb5-2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.3" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -29,4 +29,8 @@
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="MiniApp\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class CustomAppService(
|
||||
AppDatabase db,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
{
|
||||
@@ -47,15 +46,8 @@ public class CustomAppService(
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = picture.Id,
|
||||
Usage = "custom-apps.picture",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
if (request.Status == Shared.Models.CustomAppStatus.Production)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.PictureId });
|
||||
}
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
@@ -66,15 +58,8 @@ public class CustomAppService(
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = background.Id,
|
||||
Usage = "custom-apps.background",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
if (request.Status == Shared.Models.CustomAppStatus.Production)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.BackgroundId });
|
||||
}
|
||||
|
||||
db.CustomApps.Add(app);
|
||||
@@ -185,6 +170,7 @@ public class CustomAppService(
|
||||
|
||||
public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request)
|
||||
{
|
||||
var oldStatus = app.Status;
|
||||
if (request.Slug is not null)
|
||||
app.Slug = request.Slug;
|
||||
if (request.Name is not null)
|
||||
@@ -210,15 +196,8 @@ public class CustomAppService(
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = picture.Id,
|
||||
Usage = "custom-apps.picture",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
if (app.Status == Shared.Models.CustomAppStatus.Production)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.PictureId });
|
||||
}
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
@@ -229,20 +208,28 @@ public class CustomAppService(
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
new CreateReferenceRequest
|
||||
{
|
||||
FileId = background.Id,
|
||||
Usage = "custom-apps.background",
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
if (app.Status == Shared.Models.CustomAppStatus.Production)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.BackgroundId });
|
||||
}
|
||||
|
||||
db.Update(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (oldStatus != Shared.Models.CustomAppStatus.Production && app.Status == Shared.Models.CustomAppStatus.Production)
|
||||
{
|
||||
if (app.Picture is not null)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = app.Picture.Id });
|
||||
if (app.Background is not null)
|
||||
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = app.Background.Id });
|
||||
}
|
||||
else if (oldStatus == Shared.Models.CustomAppStatus.Production && app.Status != Shared.Models.CustomAppStatus.Production)
|
||||
{
|
||||
if (app.Picture is not null)
|
||||
await files.UnsetFilePublicAsync(new UnsetFilePublicRequest { FileId = app.Picture.Id });
|
||||
if (app.Background is not null)
|
||||
await files.UnsetFilePublicAsync(new UnsetFilePublicRequest { FileId = app.Background.Id });
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -257,12 +244,6 @@ public class CustomAppService(
|
||||
db.CustomApps.Remove(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = app.ResourceIdentifier
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
382
DysonNetwork.Develop/Migrations/20260117175714_AddMiniApp.Designer.cs
generated
Normal file
382
DysonNetwork.Develop/Migrations/20260117175714_AddMiniApp.Designer.cs
generated
Normal file
@@ -0,0 +1,382 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260117175714_AddMiniApp")]
|
||||
partial class AddMiniApp
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bot_accounts");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_bot_accounts_project_id");
|
||||
|
||||
b.ToTable("bot_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_apps");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_custom_apps_project_id");
|
||||
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AppId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("app_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsOidc")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_oidc");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("secret");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_app_secrets");
|
||||
|
||||
b.HasIndex("AppId")
|
||||
.HasDatabaseName("ix_custom_app_secrets_app_id");
|
||||
|
||||
b.ToTable("custom_app_secrets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Guid>("DeveloperId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_dev_projects");
|
||||
|
||||
b.HasIndex("DeveloperId")
|
||||
.HasDatabaseName("ix_dev_projects_developer_id");
|
||||
|
||||
b.ToTable("dev_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_developers");
|
||||
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<MiniAppManifest>("Manifest")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("manifest");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<int>("Stage")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("stage");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_mini_apps");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_mini_apps_project_id");
|
||||
|
||||
b.ToTable("mini_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_bot_accounts_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCustomApp", "App")
|
||||
.WithMany("Secrets")
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
|
||||
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDeveloper", "Developer")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("DeveloperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_dev_projects_developers_developer_id");
|
||||
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_mini_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.Navigation("Secrets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
|
||||
{
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
53
DysonNetwork.Develop/Migrations/20260117175714_AddMiniApp.cs
Normal file
53
DysonNetwork.Develop/Migrations/20260117175714_AddMiniApp.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMiniApp : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "mini_apps",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
stage = table.Column<int>(type: "integer", nullable: false),
|
||||
manifest = table.Column<MiniAppManifest>(type: "jsonb", nullable: false),
|
||||
project_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_mini_apps", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_mini_apps_dev_projects_project_id",
|
||||
column: x => x.project_id,
|
||||
principalTable: "dev_projects",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_mini_apps_project_id",
|
||||
table: "mini_apps",
|
||||
column: "project_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "mini_apps");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,12 @@ namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -66,7 +66,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.ToTable("bot_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -139,7 +139,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -190,24 +190,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.ToTable("custom_app_secrets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_developers");
|
||||
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -257,9 +240,73 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.ToTable("dev_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_developers");
|
||||
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<MiniAppManifest>("Manifest")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("manifest");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<int>("Stage")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("stage");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_mini_apps");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_mini_apps_project_id");
|
||||
|
||||
b.ToTable("mini_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -269,9 +316,9 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -281,9 +328,9 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCustomApp", "App")
|
||||
.WithMany("Secrets")
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -293,9 +340,9 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDeveloper", "Developer")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("DeveloperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -305,12 +352,24 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_mini_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b =>
|
||||
{
|
||||
b.Navigation("Secrets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b =>
|
||||
{
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
|
||||
185
DysonNetwork.Develop/MiniApp/MiniAppController.cs
Normal file
185
DysonNetwork.Develop/MiniApp/MiniAppController.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Develop.MiniApp;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/projects/{projectId:guid}/miniapps")]
|
||||
public class MiniAppController(MiniAppService miniAppService, Identity.DeveloperService ds, DevProjectService projectService)
|
||||
: ControllerBase
|
||||
{
|
||||
public record MiniAppRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
MiniAppStage? Stage,
|
||||
MiniAppManifest? Manifest
|
||||
);
|
||||
|
||||
public record CreateMiniAppRequest(
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(1024)]
|
||||
[RegularExpression(@"^[A-Za-z0-9_-]+$",
|
||||
ErrorMessage = "Slug can only contain letters, numbers, underscores, and hyphens.")]
|
||||
string Slug,
|
||||
|
||||
MiniAppStage Stage = MiniAppStage.Development,
|
||||
|
||||
[Required] MiniAppManifest Manifest = null!
|
||||
);
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ListMiniApps([FromRoute] string pubName, [FromRoute] Guid projectId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound("Developer not found");
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to list mini apps");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null) return NotFound("Project not found or you don't have access");
|
||||
|
||||
var miniApps = await miniAppService.GetMiniAppsByProjectAsync(projectId);
|
||||
return Ok(miniApps);
|
||||
}
|
||||
|
||||
[HttpGet("{miniAppId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetMiniApp([FromRoute] string pubName, [FromRoute] Guid projectId,
|
||||
[FromRoute] Guid miniAppId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound("Developer not found");
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to view mini app details");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null) return NotFound("Project not found or you don't have access");
|
||||
|
||||
var miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
|
||||
if (miniApp == null || miniApp.ProjectId != projectId)
|
||||
return NotFound("Mini app not found");
|
||||
|
||||
return Ok(miniApp);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CreateMiniApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromBody] CreateMiniAppRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a mini app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
try
|
||||
{
|
||||
var miniApp = await miniAppService.CreateMiniAppAsync(projectId, request.Slug, request.Stage, request.Manifest);
|
||||
return CreatedAtAction(
|
||||
nameof(GetMiniApp),
|
||||
new { pubName, projectId, miniAppId = miniApp.Id },
|
||||
miniApp
|
||||
);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{miniAppId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> UpdateMiniApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid miniAppId,
|
||||
[FromBody] MiniAppRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to update a mini app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
|
||||
if (miniApp == null || miniApp.ProjectId != projectId)
|
||||
return NotFound("Mini app not found");
|
||||
|
||||
try
|
||||
{
|
||||
miniApp = await miniAppService.UpdateMiniAppAsync(miniApp, request.Slug, request.Stage, request.Manifest);
|
||||
return Ok(miniApp);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{miniAppId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteMiniApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid miniAppId
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to delete a mini app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var miniApp = await miniAppService.GetMiniAppByIdAsync(miniAppId);
|
||||
if (miniApp == null || miniApp.ProjectId != projectId)
|
||||
return NotFound("Mini app not found");
|
||||
|
||||
var result = await miniAppService.DeleteMiniAppAsync(miniAppId);
|
||||
if (!result)
|
||||
return NotFound("Failed to delete mini app");
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
22
DysonNetwork.Develop/MiniApp/MiniAppPublicController.cs
Normal file
22
DysonNetwork.Develop/MiniApp/MiniAppPublicController.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Develop.MiniApp;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/miniapps")]
|
||||
public class MiniAppPublicController(MiniAppService miniAppService, Identity.DeveloperService developerService) : ControllerBase
|
||||
{
|
||||
[HttpGet("{slug}")]
|
||||
public async Task<ActionResult<SnMiniApp>> GetMiniAppBySlug([FromRoute] string slug)
|
||||
{
|
||||
var miniApp = await miniAppService.GetMiniAppBySlugAsync(slug);
|
||||
if (miniApp is null) return NotFound("Mini app not found");
|
||||
|
||||
var developer = await developerService.GetDeveloperById(miniApp.Project.DeveloperId);
|
||||
if (developer is null) return NotFound("Developer not found");
|
||||
miniApp.Developer = await developerService.LoadDeveloperPublisher(developer);
|
||||
|
||||
return Ok(miniApp);
|
||||
}
|
||||
}
|
||||
92
DysonNetwork.Develop/MiniApp/MiniAppService.cs
Normal file
92
DysonNetwork.Develop/MiniApp/MiniAppService.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop.MiniApp;
|
||||
|
||||
public class MiniAppService(AppDatabase db)
|
||||
{
|
||||
public async Task<SnMiniApp?> GetMiniAppByIdAsync(Guid id)
|
||||
{
|
||||
return await db.MiniApps
|
||||
.Include(m => m.Project)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
}
|
||||
|
||||
public async Task<SnMiniApp?> GetMiniAppBySlugAsync(string slug)
|
||||
{
|
||||
return await db.MiniApps
|
||||
.Include(m => m.Project)
|
||||
.FirstOrDefaultAsync(m => m.Slug == slug);
|
||||
}
|
||||
|
||||
public async Task<List<SnMiniApp>> GetMiniAppsByProjectAsync(Guid projectId)
|
||||
{
|
||||
return await db.MiniApps
|
||||
.Where(m => m.ProjectId == projectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SnMiniApp> CreateMiniAppAsync(Guid projectId, string slug, MiniAppStage stage, MiniAppManifest manifest)
|
||||
{
|
||||
var project = await db.DevProjects.FindAsync(projectId);
|
||||
if (project == null)
|
||||
throw new ArgumentException("Project not found");
|
||||
|
||||
// Check if a mini app with this slug already exists globally
|
||||
var existingMiniApp = await db.MiniApps
|
||||
.FirstOrDefaultAsync(m => m.Slug == slug);
|
||||
|
||||
if (existingMiniApp != null)
|
||||
throw new InvalidOperationException("A mini app with this slug already exists.");
|
||||
|
||||
var miniApp = new SnMiniApp
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Slug = slug,
|
||||
Stage = stage,
|
||||
Manifest = manifest,
|
||||
ProjectId = projectId,
|
||||
Project = project
|
||||
};
|
||||
|
||||
db.MiniApps.Add(miniApp);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return miniApp;
|
||||
}
|
||||
|
||||
public async Task<SnMiniApp> UpdateMiniAppAsync(SnMiniApp miniApp, string? slug, MiniAppStage? stage, MiniAppManifest? manifest)
|
||||
{
|
||||
if (slug != null && slug != miniApp.Slug)
|
||||
{
|
||||
// Check if another mini app with this slug already exists globally
|
||||
var existingMiniApp = await db.MiniApps
|
||||
.FirstOrDefaultAsync(m => m.Slug == slug && m.Id != miniApp.Id);
|
||||
|
||||
if (existingMiniApp != null)
|
||||
throw new InvalidOperationException("A mini app with this slug already exists.");
|
||||
|
||||
miniApp.Slug = slug;
|
||||
}
|
||||
|
||||
if (stage.HasValue) miniApp.Stage = stage.Value;
|
||||
if (manifest != null) miniApp.Manifest = manifest;
|
||||
|
||||
db.Update(miniApp);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return miniApp;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMiniAppAsync(Guid id)
|
||||
{
|
||||
var miniApp = await db.MiniApps.FindAsync(id);
|
||||
if (miniApp == null)
|
||||
return false;
|
||||
|
||||
db.MiniApps.Remove(miniApp);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Develop.Startup;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace DysonNetwork.Develop.Project;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/projects")]
|
||||
public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase
|
||||
public class DevProjectController(DevProjectService ps, DeveloperService ds) : ControllerBase
|
||||
{
|
||||
public record DevProjectRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
@@ -19,20 +19,20 @@ public class DevProjectController(DevProjectService projectService, DeveloperSer
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListProjects([FromRoute] string pubName)
|
||||
{
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id);
|
||||
var projects = await ps.GetProjectsByDeveloperAsync(developer.Id);
|
||||
return Ok(projects);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var project = await projectService.GetProjectAsync(id, developer.Id);
|
||||
var project = await ps.GetProjectAsync(id, developer.Id);
|
||||
if (project is null) return NotFound();
|
||||
|
||||
return Ok(project);
|
||||
@@ -45,17 +45,17 @@ public class DevProjectController(DevProjectService projectService, DeveloperSer
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a project");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest("Slug and Name are required");
|
||||
|
||||
var project = await projectService.CreateProjectAsync(developer, request);
|
||||
var project = await ps.CreateProjectAsync(developer, request);
|
||||
return CreatedAtAction(
|
||||
nameof(GetProject),
|
||||
new { pubName, id = project.Id },
|
||||
@@ -74,12 +74,15 @@ public class DevProjectController(DevProjectService projectService, DeveloperSer
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (developer is null || developer.Id != accountId)
|
||||
return Forbid();
|
||||
|
||||
var project = await projectService.UpdateProjectAsync(id, developer.Id, request);
|
||||
if (developer is null)
|
||||
return Forbid();
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Manager))
|
||||
return StatusCode(403, "You must be an manager of the developer to update a project");
|
||||
|
||||
var project = await ps.UpdateProjectAsync(id, developer.Id, request);
|
||||
if (project is null)
|
||||
return NotFound();
|
||||
|
||||
@@ -93,12 +96,14 @@ public class DevProjectController(DevProjectService projectService, DeveloperSer
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (developer is null || developer.Id != accountId)
|
||||
if (developer is null)
|
||||
return Forbid();
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Manager))
|
||||
return StatusCode(403, "You must be an manager of the developer to delete a project");
|
||||
|
||||
var success = await projectService.DeleteProjectAsync(id, developer.Id);
|
||||
var success = await ps.DeleteProjectAsync(id, developer.Id);
|
||||
if (!success)
|
||||
return NotFound();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<CustomAppService>();
|
||||
services.AddScoped<DevProjectService>();
|
||||
services.AddScoped<BotAccountService>();
|
||||
services.AddScoped<MiniApp.MiniAppService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
"PublicBasePath": "/develop"
|
||||
},
|
||||
"Cache": {
|
||||
"Serializer": "MessagePack"
|
||||
},
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
"Serializer": "JSON"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ public class AppDatabase(
|
||||
public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
|
||||
|
||||
public DbSet<SnCloudFile> Files { get; set; } = null!;
|
||||
public DbSet<SnCloudFileReference> FileReferences { get; set; } = null!;
|
||||
public DbSet<SnFileObject> FileObjects { get; set; } = null!;
|
||||
public DbSet<SnFileReplica> FileReplicas { get; set; } = null!;
|
||||
public DbSet<SnFilePermission> FilePermissions { get; set; } = null!;
|
||||
public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
|
||||
|
||||
public DbSet<PersistentTask> Tasks { get; set; } = null!;
|
||||
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Drive.Billing;
|
||||
|
||||
@@ -29,29 +30,47 @@ public class UsageService(AppDatabase db)
|
||||
public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var fileQuery = db.Files
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now)
|
||||
.Where(f => f.AccountId == accountId)
|
||||
.AsQueryable();
|
||||
|
||||
var poolUsages = await db.Pools
|
||||
.Select(p => new UsageDetails
|
||||
{
|
||||
PoolId = p.Id,
|
||||
PoolName = p.Name,
|
||||
UsageBytes = fileQuery
|
||||
.Where(f => f.PoolId == p.Id)
|
||||
.Sum(f => f.Size),
|
||||
Cost = fileQuery
|
||||
.Where(f => f.PoolId == p.Id)
|
||||
.Sum(f => f.Size) / 1024.0 / 1024.0 *
|
||||
(p.BillingConfig.CostMultiplier ?? 1.0),
|
||||
FileCount = fileQuery
|
||||
.Count(f => f.PoolId == p.Id)
|
||||
})
|
||||
var replicaData = await db.FileReplicas
|
||||
.Where(r => r.Status == SnFileReplicaStatus.Available)
|
||||
.Where(r => r.PoolId.HasValue)
|
||||
.Join(
|
||||
db.Files.Where(f => f.AccountId == accountId)
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now),
|
||||
r => r.ObjectId,
|
||||
f => f.Id,
|
||||
(r, f) => new { r.PoolId, r.ObjectId }
|
||||
)
|
||||
.Join(
|
||||
db.FileObjects,
|
||||
x => x.ObjectId,
|
||||
o => o.Id,
|
||||
(x, o) => new { x.PoolId, o.Size }
|
||||
)
|
||||
.ToListAsync();
|
||||
|
||||
var poolUsages = replicaData
|
||||
.GroupBy(r => r.PoolId!.Value)
|
||||
.Select(g =>
|
||||
{
|
||||
var poolId = g.Key;
|
||||
var pool = db.Pools.Local.FirstOrDefault(p => p.Id == poolId)
|
||||
?? db.Pools.Find(poolId);
|
||||
var multiplier = pool?.BillingConfig.CostMultiplier ?? 1.0;
|
||||
var totalBytes = g.Sum(x => x.Size);
|
||||
|
||||
return new UsageDetails
|
||||
{
|
||||
PoolId = poolId,
|
||||
PoolName = pool?.Name ?? "Unknown",
|
||||
UsageBytes = totalBytes,
|
||||
Cost = totalBytes * multiplier / 1024.0 / 1024.0,
|
||||
FileCount = g.Count()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var totalUsage = poolUsages.Sum(p => p.UsageBytes);
|
||||
var totalFileCount = poolUsages.Sum(p => p.FileCount);
|
||||
|
||||
@@ -73,17 +92,27 @@ public class UsageService(AppDatabase db)
|
||||
}
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var fileQuery = db.Files
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Where(f => f.ExpiredAt.HasValue && f.ExpiredAt > now)
|
||||
.Where(f => f.AccountId == accountId)
|
||||
.AsQueryable();
|
||||
|
||||
var usageBytes = await fileQuery
|
||||
.SumAsync(f => f.Size);
|
||||
var replicaData = await db.FileReplicas
|
||||
.Where(r => r.PoolId == poolId)
|
||||
.Where(r => r.Status == SnFileReplicaStatus.Available)
|
||||
.Join(
|
||||
db.Files.Where(f => f.AccountId == accountId)
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now),
|
||||
r => r.ObjectId,
|
||||
f => f.Id,
|
||||
(r, f) => r.ObjectId
|
||||
)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var fileCount = await fileQuery
|
||||
.CountAsync();
|
||||
var fileCount = replicaData.Count;
|
||||
|
||||
var objectIds = replicaData.Distinct().ToList();
|
||||
var usageBytes = await db.FileObjects
|
||||
.Where(o => objectIds.Contains(o.Id))
|
||||
.SumAsync(o => o.Size);
|
||||
|
||||
var cost = usageBytes / 1024.0 / 1024.0 *
|
||||
(pool.BillingConfig.CostMultiplier ?? 1.0);
|
||||
@@ -101,20 +130,23 @@ public class UsageService(AppDatabase db)
|
||||
public async Task<long> GetTotalBillableUsage(Guid accountId)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var files = await db.Files
|
||||
.Where(f => f.AccountId == accountId)
|
||||
.Where(f => f.PoolId.HasValue)
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Include(f => f.Pool)
|
||||
.Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Size,
|
||||
Multiplier = f.Pool!.BillingConfig.CostMultiplier ?? 1.0
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var totalCost = files.Sum(f => f.Size * f.Multiplier) / 1024.0 / 1024.0;
|
||||
var billingData = await (from f in db.Files
|
||||
where f.AccountId == accountId
|
||||
where !f.IsMarkedRecycle
|
||||
where !f.ExpiredAt.HasValue || f.ExpiredAt > now
|
||||
from r in f.Object!.FileReplicas
|
||||
where r.Status == SnFileReplicaStatus.Available
|
||||
where r.PoolId.HasValue
|
||||
join p in db.Pools on r.PoolId equals p.Id
|
||||
join o in db.FileObjects on r.ObjectId equals o.Id
|
||||
select new
|
||||
{
|
||||
Size = o.Size,
|
||||
Multiplier = p.BillingConfig.CostMultiplier ?? 1.0
|
||||
}).ToListAsync();
|
||||
|
||||
var totalCost = billingData.Sum(x => x.Size * x.Multiplier) / 1024.0 / 1024.0;
|
||||
|
||||
return (long)Math.Ceiling(totalCost);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -27,14 +27,13 @@
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.3" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.3" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<!-- Pin the SkiaSharp version at the 2.88.9 due to the BlurHash need this specific version -->
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -56,8 +56,8 @@ public class FileIndexController(
|
||||
{
|
||||
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Object!.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Object!.Size).ToList(),
|
||||
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
|
||||
};
|
||||
@@ -211,31 +211,31 @@ public class FileIndexController(
|
||||
|
||||
try
|
||||
{
|
||||
var filesQuery = db.Files
|
||||
var baseQuery = db.Files
|
||||
.Where(f => f.AccountId == accountId
|
||||
&& f.IsMarkedRecycle == recycled
|
||||
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)
|
||||
)
|
||||
.Include(f => f.Object)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply sorting
|
||||
filesQuery = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? filesQuery.OrderByDescending(f => f.Name)
|
||||
: filesQuery.OrderBy(f => f.Name),
|
||||
"size" => orderDesc ? filesQuery.OrderByDescending(f => f.Size)
|
||||
: filesQuery.OrderBy(f => f.Size),
|
||||
_ => orderDesc ? filesQuery.OrderByDescending(f => f.CreatedAt)
|
||||
: filesQuery.OrderBy(f => f.CreatedAt)
|
||||
};
|
||||
|
||||
if (pool.HasValue) filesQuery = filesQuery.Where(f => f.PoolId == pool);
|
||||
if (pool.HasValue) baseQuery = baseQuery.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId == pool.Value));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
filesQuery = filesQuery.Where(f => f.Name.Contains(query));
|
||||
baseQuery = baseQuery.Where(f => f.Name.Contains(query));
|
||||
}
|
||||
|
||||
var filesQuery = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? baseQuery.OrderByDescending(f => f.Name)
|
||||
: baseQuery.OrderBy(f => f.Name),
|
||||
"size" => orderDesc ? baseQuery.OrderByDescending(f => f.Object.Size)
|
||||
: baseQuery.OrderBy(f => f.Object.Size),
|
||||
_ => orderDesc ? baseQuery.OrderByDescending(f => f.CreatedAt)
|
||||
: baseQuery.OrderBy(f => f.CreatedAt)
|
||||
};
|
||||
|
||||
var totalCount = await filesQuery.CountAsync();
|
||||
|
||||
Response.Headers.Append("X-Total", totalCount.ToString());
|
||||
@@ -545,6 +545,7 @@ public class FileIndexController(
|
||||
var fileIndexes = await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.ThenInclude(f => f.Object)
|
||||
.Where(fi =>
|
||||
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
||||
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
||||
|
||||
@@ -141,6 +141,7 @@ public class FileIndexService(AppDatabase db)
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
||||
.Include(fi => fi.File)
|
||||
.ThenInclude(f => f.Object)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -154,6 +155,7 @@ public class FileIndexService(AppDatabase db)
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.FileId == fileId)
|
||||
.Include(fi => fi.File)
|
||||
.ThenInclude(f => f.Object)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -167,6 +169,7 @@ public class FileIndexService(AppDatabase db)
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.ThenInclude(f => f.Object)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
560
DysonNetwork.Drive/Migrations/20260101153809_RemoveUploadTask.Designer.cs
generated
Normal file
560
DysonNetwork.Drive/Migrations/20260101153809_RemoveUploadTask.Designer.cs
generated
Normal file
@@ -0,0 +1,560 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260101153809_RemoveUploadTask")]
|
||||
partial class RemoveUploadTask
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
155
DysonNetwork.Drive/Migrations/20260101153809_RemoveUploadTask.cs
Normal file
155
DysonNetwork.Drive/Migrations/20260101153809_RemoveUploadTask.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUploadTask : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "bundle_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunk_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_count",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "content_type",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "discriminator",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encrypt_password",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_name",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "hash",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "path",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "pool_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "bundle_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "chunk_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_count",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "content_type",
|
||||
table: "tasks",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "discriminator",
|
||||
table: "tasks",
|
||||
type: "character varying(21)",
|
||||
maxLength: 21,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "encrypt_password",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "file_name",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "file_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "hash",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "path",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<int>>(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks",
|
||||
type: "integer[]",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
632
DysonNetwork.Drive/Migrations/20260101154612_RollbackRemoveUploadTask.Designer.cs
generated
Normal file
632
DysonNetwork.Drive/Migrations/20260101154612_RollbackRemoveUploadTask.Designer.cs
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260101154612_RollbackRemoveUploadTask")]
|
||||
partial class RollbackRemoveUploadTask
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RollbackRemoveUploadTask : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "bundle_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "chunk_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_count",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "content_type",
|
||||
table: "tasks",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "discriminator",
|
||||
table: "tasks",
|
||||
type: "character varying(21)",
|
||||
maxLength: 21,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "encrypt_password",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "file_name",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "file_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "hash",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "path",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<int>>(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks",
|
||||
type: "integer[]",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "bundle_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunk_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_count",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "content_type",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "discriminator",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encrypt_password",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_name",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "hash",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "path",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "pool_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
762
DysonNetwork.Drive/Migrations/20260110084758_RemoveFileReferencesAndAddFileObjectOwner.Designer.cs
generated
Normal file
762
DysonNetwork.Drive/Migrations/20260110084758_RemoveFileReferencesAndAddFileObjectOwner.Designer.cs
generated
Normal file
@@ -0,0 +1,762 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260110084758_RemoveFileReferencesAndAddFileObjectOwner")]
|
||||
partial class RemoveFileReferencesAndAddFileObjectOwner
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveFileReferencesAndAddFileObjectOwner : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_references");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "object_id",
|
||||
table: "files",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_objects",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
size = table.Column<long>(type: "bigint", nullable: false),
|
||||
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
|
||||
mime_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
hash = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
has_compression = table.Column<bool>(type: "boolean", nullable: false),
|
||||
has_thumbnail = table.Column<bool>(type: "boolean", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_objects", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
file_id = table.Column<string>(type: "text", nullable: false),
|
||||
subject_type = table.Column<int>(type: "integer", nullable: false),
|
||||
subject_id = table.Column<string>(type: "text", nullable: false),
|
||||
permission = table.Column<int>(type: "integer", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_permissions", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_replicas",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
object_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
pool_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
storage_id = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
is_primary = table.Column<bool>(type: "boolean", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_replicas", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_replicas_file_objects_object_id",
|
||||
column: x => x.object_id,
|
||||
principalTable: "file_objects",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_replicas_pools_pool_id",
|
||||
column: x => x.pool_id,
|
||||
principalTable: "pools",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_files_object_id",
|
||||
table: "files",
|
||||
column: "object_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_replicas_object_id",
|
||||
table: "file_replicas",
|
||||
column: "object_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_replicas_pool_id",
|
||||
table: "file_replicas",
|
||||
column: "pool_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_files_file_objects_object_id",
|
||||
table: "files",
|
||||
column: "object_id",
|
||||
principalTable: "file_objects",
|
||||
principalColumn: "id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_files_file_objects_object_id",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_replicas");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_objects");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_files_object_id",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "object_id",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_references",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
resource_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
usage = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_references", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_references_files_file_id",
|
||||
column: x => x.file_id,
|
||||
principalTable: "files",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_references_file_id",
|
||||
table: "file_references",
|
||||
column: "file_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
760
DysonNetwork.Drive/Migrations/20260110142132_NullableReplicaPoolId.Designer.cs
generated
Normal file
760
DysonNetwork.Drive/Migrations/20260110142132_NullableReplicaPoolId.Designer.cs
generated
Normal file
@@ -0,0 +1,760 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260110142132_NullableReplicaPoolId")]
|
||||
partial class NullableReplicaPoolId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class NullableReplicaPoolId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_file_replicas_pools_pool_id",
|
||||
table: "file_replicas");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "file_replicas",
|
||||
type: "uuid",
|
||||
nullable: true,
|
||||
oldClrType: typeof(Guid),
|
||||
oldType: "uuid");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_file_replicas_pools_pool_id",
|
||||
table: "file_replicas",
|
||||
column: "pool_id",
|
||||
principalTable: "pools",
|
||||
principalColumn: "id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_file_replicas_pools_pool_id",
|
||||
table: "file_replicas");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "file_replicas",
|
||||
type: "uuid",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||
oldClrType: typeof(Guid),
|
||||
oldType: "uuid",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_file_replicas_pools_pool_id",
|
||||
table: "file_replicas",
|
||||
column: "pool_id",
|
||||
principalTable: "pools",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
688
DysonNetwork.Drive/Migrations/20260110154021_RemoveUploadTaskAgain.Designer.cs
generated
Normal file
688
DysonNetwork.Drive/Migrations/20260110154021_RemoveUploadTaskAgain.Designer.cs
generated
Normal file
@@ -0,0 +1,688 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260110154021_RemoveUploadTaskAgain")]
|
||||
partial class RemoveUploadTaskAgain
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUploadTaskAgain : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "bundle_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunk_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_count",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "content_type",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "discriminator",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "encrypt_password",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_name",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_size",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "hash",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "path",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "pool_id",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "bundle_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "chunk_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_count",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "chunks_uploaded",
|
||||
table: "tasks",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "content_type",
|
||||
table: "tasks",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "discriminator",
|
||||
table: "tasks",
|
||||
type: "character varying(21)",
|
||||
maxLength: 21,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "encrypt_password",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "file_name",
|
||||
table: "tasks",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "file_size",
|
||||
table: "tasks",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "hash",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "path",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "tasks",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<int>>(
|
||||
name: "uploaded_chunks",
|
||||
table: "tasks",
|
||||
type: "integer[]",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
658
DysonNetwork.Drive/Migrations/20260111152243_CleanCloudFile.Designer.cs
generated
Normal file
658
DysonNetwork.Drive/Migrations/20260111152243_CleanCloudFile.Designer.cs
generated
Normal file
@@ -0,0 +1,658 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260111152243_CleanCloudFile")]
|
||||
partial class CleanCloudFile
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CleanCloudFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "file_meta",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "has_compression",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "has_thumbnail",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "hash",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_encrypted",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "mime_type",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "size",
|
||||
table: "files");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Dictionary<string, object>>(
|
||||
name: "file_meta",
|
||||
table: "files",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "has_compression",
|
||||
table: "files",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "has_thumbnail",
|
||||
table: "files",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "hash",
|
||||
table: "files",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_encrypted",
|
||||
table: "files",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "mime_type",
|
||||
table: "files",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "size",
|
||||
table: "files",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
654
DysonNetwork.Drive/Migrations/20260112170805_RemoveAccountFromFileObject.Designer.cs
generated
Normal file
654
DysonNetwork.Drive/Migrations/20260112170805_RemoveAccountFromFileObject.Designer.cs
generated
Normal file
@@ -0,0 +1,654 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260112170805_RemoveAccountFromFileObject")]
|
||||
partial class RemoveAccountFromFileObject
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveAccountFromFileObject : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "account_id",
|
||||
table: "file_objects");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "account_id",
|
||||
table: "file_objects",
|
||||
type: "uuid",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
}
|
||||
}
|
||||
}
|
||||
640
DysonNetwork.Drive/Migrations/20260113152536_RemovePoolFromCloudFile.Designer.cs
generated
Normal file
640
DysonNetwork.Drive/Migrations/20260113152536_RemovePoolFromCloudFile.Designer.cs
generated
Normal file
@@ -0,0 +1,640 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260113152536_RemovePoolFromCloudFile")]
|
||||
partial class RemovePoolFromCloudFile
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Object");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemovePoolFromCloudFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_files_pools_pool_id",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_files_pool_id",
|
||||
table: "files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "pool_id",
|
||||
table: "files");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "pool_id",
|
||||
table: "files",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_files_pool_id",
|
||||
table: "files",
|
||||
column: "pool_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_files_pools_pool_id",
|
||||
table: "files",
|
||||
column: "pool_id",
|
||||
principalTable: "pools",
|
||||
principalColumn: "id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -100,12 +100,6 @@ namespace DysonNetwork.Drive.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
@@ -173,60 +167,6 @@ namespace DysonNetwork.Drive.Migrations
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
@@ -321,54 +261,25 @@ namespace DysonNetwork.Drive.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
b.Property<string>("ObjectId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||
b.PrimitiveCollection<string>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
@@ -397,8 +308,8 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_files_object_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
@@ -509,78 +420,153 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("path");
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_objects");
|
||||
|
||||
b.ToTable("file_objects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("permission");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("subject_id");
|
||||
|
||||
b.Property<int>("SubjectType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("subject_type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_permissions");
|
||||
|
||||
b.ToTable("file_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_primary");
|
||||
|
||||
b.Property<string>("ObjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("object_id");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_replicas");
|
||||
|
||||
b.Navigation("File");
|
||||
b.HasIndex("ObjectId")
|
||||
.HasDatabaseName("ix_file_replicas_object_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_file_replicas_pool_id");
|
||||
|
||||
b.ToTable("file_replicas", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
@@ -590,14 +576,14 @@ namespace DysonNetwork.Drive.Migrations
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
.HasForeignKey("ObjectId")
|
||||
.HasConstraintName("fk_files_file_objects_object_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
b.Navigation("Object");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
@@ -612,17 +598,39 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
|
||||
.WithMany("FileReplicas")
|
||||
.HasForeignKey("ObjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_replicas_file_objects_object_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_file_replicas_pools_pool_id");
|
||||
|
||||
b.Navigation("Object");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
|
||||
{
|
||||
b.Navigation("FileReplicas");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Startup;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -21,7 +21,7 @@ builder.Services.AddRingService();
|
||||
builder.Services.AddAccountService();
|
||||
|
||||
builder.Services.AddAppFlushHandlers();
|
||||
builder.Services.AddAppBusinessServices();
|
||||
builder.Services.AddAppBusinessServices(builder.Configuration);
|
||||
builder.Services.AddAppScheduledJobs();
|
||||
|
||||
builder.AddSwaggerManifest(
|
||||
|
||||
@@ -16,7 +16,6 @@ public static class ApplicationBuilderExtensions
|
||||
{
|
||||
// Map your gRPC services here
|
||||
app.MapGrpcService<FileServiceGrpc>();
|
||||
app.MapGrpcService<FileReferenceServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
|
||||
@@ -3,7 +3,7 @@ using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Drive.Storage.Model;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Stream;
|
||||
using DysonNetwork.Shared.Queue;
|
||||
using FFMpegCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NATS.Client.Core;
|
||||
@@ -156,18 +156,45 @@ public class BroadcastEventHandler(
|
||||
|
||||
logger.LogInformation("Processing file {FileId} in background...", fileId);
|
||||
|
||||
var fileToUpdate = await scopedDb.Files.AsNoTracking().FirstAsync(f => f.Id == fileId);
|
||||
var fileToUpdate = await scopedDb.Files
|
||||
.AsNoTracking()
|
||||
.Include(f => f.Object)
|
||||
.FirstAsync(f => f.Id == fileId);
|
||||
|
||||
// Find the upload task associated with this file
|
||||
var uploadTask = await scopedDb.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.FirstOrDefaultAsync(t => t.FileName == fileToUpdate.Name && t.FileSize == fileToUpdate.Size);
|
||||
var baseTask = await scopedDb.Tasks
|
||||
.Where(t => t.Type == TaskType.FileUpload)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (fileToUpdate.IsEncrypted)
|
||||
var uploadTask = baseTask != null ? new PersistentUploadTask
|
||||
{
|
||||
uploads.Add((processingFilePath, string.Empty, contentType, false));
|
||||
Id = baseTask.Id,
|
||||
TaskId = baseTask.TaskId,
|
||||
Name = baseTask.Name,
|
||||
Description = baseTask.Description,
|
||||
Type = baseTask.Type,
|
||||
Status = baseTask.Status,
|
||||
AccountId = baseTask.AccountId,
|
||||
Progress = baseTask.Progress,
|
||||
Parameters = baseTask.Parameters,
|
||||
Results = baseTask.Results,
|
||||
ErrorMessage = baseTask.ErrorMessage,
|
||||
StartedAt = baseTask.StartedAt,
|
||||
CompletedAt = baseTask.CompletedAt,
|
||||
ExpiredAt = baseTask.ExpiredAt,
|
||||
LastActivity = baseTask.LastActivity,
|
||||
Priority = baseTask.Priority,
|
||||
EstimatedDurationSeconds = baseTask.EstimatedDurationSeconds,
|
||||
CreatedAt = baseTask.CreatedAt,
|
||||
UpdatedAt = baseTask.UpdatedAt
|
||||
} : null;
|
||||
|
||||
if (uploadTask != null && (uploadTask.FileName != fileToUpdate.Name || uploadTask.FileSize != fileToUpdate.Size))
|
||||
{
|
||||
uploadTask = null;
|
||||
}
|
||||
else if (!pool.PolicyConfig.NoOptimization)
|
||||
|
||||
if (!pool.PolicyConfig.NoOptimization)
|
||||
{
|
||||
var fileExtension = Path.GetExtension(processingFilePath);
|
||||
switch (contentType.Split('/')[0])
|
||||
@@ -287,12 +314,26 @@ public class BroadcastEventHandler(
|
||||
logger.LogInformation("Uploaded file {FileId} done!", fileId);
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var newReplica = new SnFileReplica
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ObjectId = fileId,
|
||||
PoolId = destPool,
|
||||
StorageId = storageId,
|
||||
Status = SnFileReplicaStatus.Available,
|
||||
IsPrimary = false
|
||||
};
|
||||
scopedDb.FileReplicas.Add(newReplica);
|
||||
|
||||
await scopedDb.Files.Where(f => f.Id == fileId).ExecuteUpdateAsync(setter => setter
|
||||
.SetProperty(f => f.UploadedAt, now)
|
||||
.SetProperty(f => f.PoolId, destPool)
|
||||
.SetProperty(f => f.MimeType, newMimeType)
|
||||
.SetProperty(f => f.HasCompression, hasCompression)
|
||||
.SetProperty(f => f.HasThumbnail, hasThumbnail)
|
||||
);
|
||||
|
||||
await scopedDb.FileObjects.Where(fo => fo.Id == fileId).ExecuteUpdateAsync(setter => setter
|
||||
.SetProperty(fo => fo.MimeType, newMimeType)
|
||||
.SetProperty(fo => fo.HasCompression, hasCompression)
|
||||
.SetProperty(fo => fo.HasThumbnail, hasThumbnail)
|
||||
);
|
||||
|
||||
// Only delete temp file after successful upload and db update
|
||||
|
||||
@@ -29,6 +29,13 @@ public static class ScheduledJobsConfiguration
|
||||
.ForJob(persistentTaskCleanupJob)
|
||||
.WithIdentity("PersistentTaskCleanupTrigger")
|
||||
.WithCronSchedule("0 0 2 * * ?")); // Run daily at 2 AM
|
||||
|
||||
var fileObjectCleanupJob = new JobKey("FileObjectCleanup");
|
||||
q.AddJob<FileObjectCleanupJob>(opts => opts.WithIdentity(fileObjectCleanupJob));
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(fileObjectCleanupJob)
|
||||
.WithIdentity("FileObjectCleanupTrigger")
|
||||
.WithCronSchedule("0 0 1 * * ?")); // Run daily at 1 AM
|
||||
});
|
||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||
|
||||
|
||||
@@ -9,58 +9,64 @@ namespace DysonNetwork.Drive.Startup;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
||||
extension(IServiceCollection services)
|
||||
{
|
||||
services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
// Register gRPC services
|
||||
services.AddGrpc(options =>
|
||||
public IServiceCollection AddAppServices(IConfiguration configuration)
|
||||
{
|
||||
options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
services.AddGrpcReflection();
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
services.AddHttpClient();
|
||||
|
||||
// Register gRPC services
|
||||
services.AddGrpc(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public IServiceCollection AddAppAuthentication()
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
public IServiceCollection AddAppFlushHandlers()
|
||||
{
|
||||
services.AddSingleton<FlushBufferService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
public IServiceCollection AddAppBusinessServices(IConfiguration configuration)
|
||||
{
|
||||
services.Configure<Storage.Options.FileReanalysisOptions>(configuration.GetSection("FileReanalysis"));
|
||||
|
||||
public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<FlushBufferService>();
|
||||
services.AddScoped<Storage.FileService>();
|
||||
services.AddScoped<Storage.FileReanalysisService>();
|
||||
services.AddScoped<Storage.PersistentTaskService>();
|
||||
services.AddScoped<FileIndexService>();
|
||||
services.AddScoped<Billing.UsageService>();
|
||||
services.AddScoped<Billing.QuotaService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
services.AddHostedService<BroadcastEventHandler>();
|
||||
services.AddHostedService<Storage.FileReanalysisBackgroundService>();
|
||||
|
||||
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<Storage.FileService>();
|
||||
services.AddScoped<Storage.FileReferenceService>();
|
||||
services.AddScoped<Storage.PersistentTaskService>();
|
||||
services.AddScoped<FileIndexService>();
|
||||
services.AddScoped<Billing.UsageService>();
|
||||
services.AddScoped<Billing.QuotaService>();
|
||||
|
||||
services.AddHostedService<BroadcastEventHandler>();
|
||||
|
||||
return services;
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
@@ -40,14 +41,16 @@ public class CloudFileUnusedRecyclingJob(
|
||||
var markedCount = 0;
|
||||
var totalFiles = await db.Files
|
||||
.Where(f => f.FileIndexes.Count == 0)
|
||||
.Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value))
|
||||
.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId.HasValue && recyclablePools.Contains(r.PoolId.Value)))
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o.FileReplicas)
|
||||
.CountAsync();
|
||||
|
||||
logger.LogInformation("Found {TotalFiles} files to check for unused status", totalFiles);
|
||||
|
||||
// Define a timestamp to limit the age of files we're processing in this run
|
||||
// This spreads the processing across multiple job runs for very large databases
|
||||
// This spreads processing across multiple job runs for very large databases
|
||||
var ageThreshold = now - Duration.FromDays(30); // Process files up to 90 days old in this run
|
||||
|
||||
// Instead of loading all files at once, use pagination
|
||||
@@ -56,17 +59,18 @@ public class CloudFileUnusedRecyclingJob(
|
||||
|
||||
while (hasMoreFiles)
|
||||
{
|
||||
// Query for the next batch of files using keyset pagination
|
||||
var filesQuery = db.Files
|
||||
.Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value))
|
||||
IQueryable<SnCloudFile> baseQuery = db.Files
|
||||
.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId.HasValue && recyclablePools.Contains(r.PoolId.Value)))
|
||||
.Where(f => !f.IsMarkedRecycle)
|
||||
.Where(f => f.CreatedAt <= ageThreshold); // Only process older files first
|
||||
.Where(f => f.CreatedAt <= ageThreshold)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o.FileReplicas);
|
||||
|
||||
if (lastProcessedId != null)
|
||||
filesQuery = filesQuery.Where(f => string.Compare(f.Id, lastProcessedId) > 0);
|
||||
baseQuery = baseQuery.Where(f => string.Compare(f.Id, lastProcessedId) > 0);
|
||||
|
||||
var fileBatch = await filesQuery
|
||||
.OrderBy(f => f.Id) // Ensure consistent ordering for pagination
|
||||
var fileBatch = await baseQuery
|
||||
.OrderBy(f => f.Id)
|
||||
.Take(batchSize)
|
||||
.Select(f => f.Id)
|
||||
.ToListAsync();
|
||||
@@ -80,13 +84,11 @@ public class CloudFileUnusedRecyclingJob(
|
||||
processedCount += fileBatch.Count;
|
||||
lastProcessedId = fileBatch.Last();
|
||||
|
||||
// Optimized query: Find files that have no references OR all references are expired
|
||||
// This replaces the memory-intensive approach of loading all references
|
||||
// Optimized query: Find files that have no file object or no replicas
|
||||
// A file is considered "unused" if its file object has no replicas
|
||||
var filesToMark = await db.Files
|
||||
.Where(f => fileBatch.Contains(f.Id))
|
||||
.Where(f => !db.FileReferences.Any(r => r.FileId == f.Id) || // No references at all
|
||||
!db.FileReferences.Any(r => r.FileId == f.Id && // OR has references but all are expired
|
||||
(r.ExpiredAt == null || r.ExpiredAt > now)))
|
||||
.Where(f => f.Object == null || f.Object.FileReplicas.Count == 0)
|
||||
.Select(f => f.Id)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
@@ -14,10 +16,14 @@ public class FileController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
IConfiguration configuration,
|
||||
IWebHostEnvironment env,
|
||||
FileReferenceService fileReferenceService
|
||||
IWebHostEnvironment env
|
||||
) : ControllerBase
|
||||
{
|
||||
private string AccessTokenSecret => configuration["AccessToken:Secret"]
|
||||
?? "dyson-network-default-access-token-secret-change-in-production";
|
||||
|
||||
private static readonly TimeSpan LocalSignedUrlExpiry = TimeSpan.FromMinutes(10);
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult> OpenFile(
|
||||
string id,
|
||||
@@ -32,7 +38,8 @@ public class FileController(
|
||||
var file = await fs.GetFileAsync(fileId);
|
||||
if (file is null) return NotFound("File not found.");
|
||||
|
||||
var accessResult = await ValidateFileAccess(file, passcode);
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
var accessResult = await ValidateFileAccess(file, passcode, currentUser);
|
||||
if (accessResult is not null) return accessResult;
|
||||
|
||||
// Handle direct storage URL redirect
|
||||
@@ -47,7 +54,7 @@ public class FileController(
|
||||
return await ServeRemoteFile(file, fileExtension, download, original, thumbnail, overrideMimeType);
|
||||
}
|
||||
|
||||
private (string fileId, string? extension) ParseFileId(string id)
|
||||
private static (string fileId, string? extension) ParseFileId(string id)
|
||||
{
|
||||
if (!id.Contains('.')) return (id, null);
|
||||
|
||||
@@ -55,38 +62,186 @@ public class FileController(
|
||||
return (parts.First(), parts.Last());
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> ValidateFileAccess(SnCloudFile file, string? passcode)
|
||||
private async Task<ActionResult?> ValidateFileAccess(SnCloudFile file, string? passcode,
|
||||
Account? currentUser = null)
|
||||
{
|
||||
if (file.Bundle is not null && !file.Bundle.VerifyPasscode(passcode))
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "The passcode is incorrect.");
|
||||
return null;
|
||||
|
||||
var hasAccess = await CheckFilePermissionAsync(file, currentUser, SnFilePermissionLevel.Read);
|
||||
return !hasAccess
|
||||
? StatusCode(StatusCodes.Status403Forbidden, "You don't have permission to access this file.")
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckFilePermissionAsync(
|
||||
SnCloudFile file,
|
||||
Account? currentUser,
|
||||
SnFilePermissionLevel requiredLevel
|
||||
)
|
||||
{
|
||||
if (currentUser?.IsSuperuser == true)
|
||||
return true;
|
||||
|
||||
Guid? accountId = currentUser is not null ? Guid.Parse(currentUser.Id) : null;
|
||||
if (file.AccountId == accountId)
|
||||
return true;
|
||||
|
||||
var permissions = await db.FilePermissions
|
||||
.Where(p => p.FileId == file.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var perm in permissions)
|
||||
{
|
||||
switch (perm.SubjectType)
|
||||
{
|
||||
case SnFilePermissionType.Anyone:
|
||||
case SnFilePermissionType.Someone when currentUser != null && perm.SubjectId == currentUser.Id:
|
||||
if (requiredLevel == SnFilePermissionLevel.Read ||
|
||||
(requiredLevel == SnFilePermissionLevel.Write && perm.Permission == SnFilePermissionLevel.Write))
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> HasWritePermissionAsync(SnCloudFile file, Account? currentUser)
|
||||
{
|
||||
if (currentUser?.IsSuperuser == true)
|
||||
return true;
|
||||
|
||||
if (currentUser is not null && file.AccountId == Guid.Parse(currentUser.Id))
|
||||
return true;
|
||||
|
||||
var permissions = await db.FilePermissions
|
||||
.Where(p => p.FileId == file.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var perm in permissions)
|
||||
{
|
||||
if (perm.Permission != SnFilePermissionLevel.Write) continue;
|
||||
|
||||
switch (perm.SubjectType)
|
||||
{
|
||||
case SnFilePermissionType.Anyone:
|
||||
return true;
|
||||
case SnFilePermissionType.Someone when currentUser != null && perm.SubjectId == currentUser.Id:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Task<ActionResult> ServeLocalFile(SnCloudFile file)
|
||||
{
|
||||
// Try temp storage first
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
var hasWritePermission = Task.Run(() => HasWritePermissionAsync(file, currentUser)).GetAwaiter().GetResult();
|
||||
var accessToken = GenerateLocalSignedToken(file.Id, currentUser?.Id, hasWritePermission);
|
||||
|
||||
var gatewayUrl = configuration["GatewayUrl"];
|
||||
var accessUrl = $"{gatewayUrl}/drive/files/{file.Id}/access?token={accessToken}";
|
||||
return Task.FromResult<ActionResult>(Redirect(accessUrl));
|
||||
}
|
||||
|
||||
[HttpGet("{id}/access")]
|
||||
public async Task<ActionResult> AccessFile(string id, [FromQuery] string token)
|
||||
{
|
||||
var validation = ValidateLocalSignedToken(token);
|
||||
if (!validation.IsValid)
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid or expired access token.");
|
||||
|
||||
if (validation.FileId != id)
|
||||
return StatusCode(StatusCodes.Status400BadRequest, "Token mismatch.");
|
||||
|
||||
var file = await fs.GetFileAsync(id);
|
||||
if (file is null) return NotFound("File not found.");
|
||||
|
||||
var tempFilePath = Path.Combine(Path.GetTempPath(), file.Id);
|
||||
if (System.IO.File.Exists(tempFilePath))
|
||||
{
|
||||
if (file.IsEncrypted)
|
||||
return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status403Forbidden,
|
||||
"Encrypted files cannot be accessed before they are processed and stored."));
|
||||
|
||||
return Task.FromResult<ActionResult>(PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream",
|
||||
file.Name, enableRangeProcessing: true));
|
||||
return PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream",
|
||||
file.Name, enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
// Fallback for tus uploads
|
||||
var tusStorePath = configuration.GetValue<string>("Storage:Uploads");
|
||||
if (string.IsNullOrEmpty(tusStorePath))
|
||||
return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status400BadRequest,
|
||||
"File is being processed. Please try again later."));
|
||||
return StatusCode(StatusCodes.Status400BadRequest,
|
||||
"File is being processed. Please try again later.");
|
||||
var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
|
||||
return System.IO.File.Exists(tusFilePath)
|
||||
? Task.FromResult<ActionResult>(PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream",
|
||||
file.Name, enableRangeProcessing: true))
|
||||
: Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status400BadRequest,
|
||||
"File is being processed. Please try again later."));
|
||||
if (System.IO.File.Exists(tusFilePath))
|
||||
{
|
||||
return PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream",
|
||||
file.Name, enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status400BadRequest,
|
||||
"File is being processed. Please try again later.");
|
||||
}
|
||||
|
||||
private string GenerateLocalSignedToken(string fileId, string? userId, bool hasWritePermission)
|
||||
{
|
||||
var expiry = DateTimeOffset.UtcNow.Add(LocalSignedUrlExpiry).ToUnixTimeSeconds();
|
||||
var payload = $"{fileId}|{userId ?? ""}|{expiry}|{hasWritePermission}";
|
||||
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
var signature = ComputeHmacSignature(payloadBase64);
|
||||
var token = $"{payloadBase64}.{signature}";
|
||||
|
||||
return Uri.EscapeDataString(token);
|
||||
}
|
||||
|
||||
private (bool IsValid, string FileId, string? UserId, bool HasWritePermission) ValidateLocalSignedToken(
|
||||
string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenDecoded = Uri.UnescapeDataString(token);
|
||||
var parts = tokenDecoded.Split('.');
|
||||
if (parts.Length != 2)
|
||||
return (false, string.Empty, null, false);
|
||||
|
||||
var payloadBase64 = parts[0];
|
||||
var providedSignature = parts[1];
|
||||
|
||||
var expectedSignature = ComputeHmacSignature(payloadBase64);
|
||||
if (!CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(expectedSignature),
|
||||
Encoding.UTF8.GetBytes(providedSignature)))
|
||||
return (false, string.Empty, null, false);
|
||||
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64);
|
||||
var payload = Encoding.UTF8.GetString(payloadBytes);
|
||||
var payloadParts = payload.Split('|');
|
||||
|
||||
if (payloadParts.Length < 4)
|
||||
return (false, string.Empty, null, false);
|
||||
|
||||
var fileId = payloadParts[0];
|
||||
var userId = string.IsNullOrEmpty(payloadParts[1]) ? null : payloadParts[1];
|
||||
var expiry = long.Parse(payloadParts[2]);
|
||||
var hasWritePermission = bool.Parse(payloadParts[3]);
|
||||
|
||||
if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiry)
|
||||
return (false, string.Empty, null, false);
|
||||
|
||||
return (true, fileId, userId, hasWritePermission);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, string.Empty, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private string ComputeHmacSignature(string data)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(AccessTokenSecret));
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private async Task<ActionResult> ServeRemoteFile(
|
||||
@@ -98,11 +253,12 @@ public class FileController(
|
||||
string? overrideMimeType
|
||||
)
|
||||
{
|
||||
if (!file.PoolId.HasValue)
|
||||
var primaryReplica = file.Object?.FileReplicas.FirstOrDefault(r => r.IsPrimary);
|
||||
if (primaryReplica == null || primaryReplica.PoolId == null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
"File is in an inconsistent state: uploaded but no pool ID.");
|
||||
|
||||
var pool = await fs.GetPoolAsync(file.PoolId.Value);
|
||||
var pool = await fs.GetPoolAsync(primaryReplica.PoolId.Value);
|
||||
if (pool is null)
|
||||
return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible.");
|
||||
|
||||
@@ -145,9 +301,7 @@ public class FileController(
|
||||
private ActionResult? TryProxyRedirect(SnCloudFile file, RemoteStorageConfig dest, string fileName)
|
||||
{
|
||||
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
|
||||
{
|
||||
return Redirect(BuildProxyUrl(dest.ImageProxy, fileName));
|
||||
}
|
||||
|
||||
return dest.AccessProxy is not null ? Redirect(BuildProxyUrl(dest.AccessProxy, fileName)) : null;
|
||||
}
|
||||
@@ -168,7 +322,7 @@ public class FileController(
|
||||
string? overrideMimeType
|
||||
)
|
||||
{
|
||||
var client = fs.CreateMinioClient(dest);
|
||||
var client = FileService.CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
return BadRequest("Failed to configure client for remote destination, file got an invalid storage remote.");
|
||||
|
||||
@@ -182,6 +336,9 @@ public class FileController(
|
||||
.WithHeaders(headers)
|
||||
);
|
||||
|
||||
if (dest.AccessEndpoint is not null)
|
||||
openUrl = openUrl.Replace($"{dest.Endpoint}/{dest.Bucket}", dest.AccessEndpoint);
|
||||
|
||||
return Redirect(openUrl);
|
||||
}
|
||||
|
||||
@@ -231,17 +388,18 @@ public class FileController(
|
||||
}
|
||||
|
||||
[HttpGet("{id}/references")]
|
||||
public async Task<ActionResult<List<Shared.Models.SnCloudFileReference>>> GetFileReferences(string id)
|
||||
public async Task<ActionResult<List<SnCloudFile>>> GetFileReferences(string id)
|
||||
{
|
||||
var file = await fs.GetFileAsync(id);
|
||||
if (file is null) return NotFound("File not found.");
|
||||
|
||||
// Check if user has access to the file
|
||||
var accessResult = await ValidateFileAccess(file, null);
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
var accessResult = await ValidateFileAccess(file, null, currentUser);
|
||||
if (accessResult is not null) return accessResult;
|
||||
|
||||
// Get references using the injected FileReferenceService
|
||||
var references = await fileReferenceService.GetReferencesAsync(id);
|
||||
var references = await db.Files
|
||||
.Where(f => f.ObjectId == file.ObjectId && f.Id != file.Id)
|
||||
.ToListAsync();
|
||||
return Ok(references);
|
||||
}
|
||||
|
||||
@@ -304,10 +462,10 @@ public class FileController(
|
||||
var filesQuery = db.Files
|
||||
.Where(e => e.IsMarkedRecycle == recycled)
|
||||
.Where(e => e.AccountId == accountId)
|
||||
.Include(e => e.Pool)
|
||||
.Include(e => e.Object)
|
||||
.AsQueryable();
|
||||
|
||||
if (pool.HasValue) filesQuery = filesQuery.Where(e => e.PoolId == pool);
|
||||
if (pool.HasValue) filesQuery = filesQuery.Where(e => e.Object!.FileReplicas.Any(r => r.PoolId == pool.Value));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Job responsible for cleaning up expired file references
|
||||
/// </summary>
|
||||
public class FileExpirationJob(AppDatabase db, FileService fileService, ILogger<FileExpirationJob> logger) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
logger.LogInformation("Running file reference expiration job at {now}", now);
|
||||
|
||||
// Delete expired references in bulk and get affected file IDs
|
||||
var affectedFileIds = await db.FileReferences
|
||||
.Where(r => r.ExpiredAt < now && r.ExpiredAt != null)
|
||||
.Select(r => r.FileId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
if (!affectedFileIds.Any())
|
||||
{
|
||||
logger.LogInformation("No expired file references found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Found expired references for {count} files", affectedFileIds.Count);
|
||||
|
||||
// Delete expired references in bulk
|
||||
var deletedReferencesCount = await db.FileReferences
|
||||
.Where(r => r.ExpiredAt < now && r.ExpiredAt != null)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
logger.LogInformation("Deleted {count} expired file references", deletedReferencesCount);
|
||||
|
||||
// Find files that now have no remaining references (bulk operation)
|
||||
var filesToDelete = await db.Files
|
||||
.Where(f => affectedFileIds.Contains(f.Id))
|
||||
.Where(f => !db.FileReferences.Any(r => r.FileId == f.Id))
|
||||
.Select(f => f.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (filesToDelete.Any())
|
||||
{
|
||||
logger.LogInformation("Deleting {count} files that have no remaining references", filesToDelete.Count);
|
||||
|
||||
// Get files for deletion
|
||||
var files = await db.Files
|
||||
.Where(f => filesToDelete.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
// Delete files and their data in parallel
|
||||
var deleteTasks = files.Select(f => fileService.DeleteFileAsync(f));
|
||||
await Task.WhenAll(deleteTasks);
|
||||
}
|
||||
|
||||
// Purge cache for files that still have references
|
||||
var filesWithRemainingRefs = affectedFileIds.Except(filesToDelete).ToList();
|
||||
if (filesWithRemainingRefs.Any())
|
||||
{
|
||||
var cachePurgeTasks = filesWithRemainingRefs.Select(fileService._PurgeCacheAsync);
|
||||
await Task.WhenAll(cachePurgeTasks);
|
||||
}
|
||||
|
||||
logger.LogInformation("Completed file reference expiration job");
|
||||
}
|
||||
}
|
||||
97
DysonNetwork.Drive/Storage/FileObjectCleanupJob.cs
Normal file
97
DysonNetwork.Drive/Storage/FileObjectCleanupJob.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio.DataModel.Args;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Job responsible for cleaning up orphaned file objects
|
||||
/// When no SnCloudFile references a SnFileObject, the file object is considered orphaned
|
||||
/// and should be deleted from disk and database
|
||||
/// </summary>
|
||||
public class FileObjectCleanupJob(AppDatabase db, FileService fileService, ILogger<FileObjectCleanupJob> logger) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
logger.LogInformation("Running file object cleanup job at {now}", now);
|
||||
|
||||
// Find orphaned file objects (objects with no cloud files referencing them)
|
||||
var referencedObjectIds = await db.Files
|
||||
.Where(f => f.ObjectId != null)
|
||||
.Select(f => f.ObjectId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var orphanedObjects = await db.FileObjects
|
||||
.Where(fo => !referencedObjectIds.Contains(fo.Id))
|
||||
.ToListAsync();
|
||||
|
||||
if (!orphanedObjects.Any())
|
||||
{
|
||||
logger.LogInformation("No orphaned file objects found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Found {count} orphaned file objects", orphanedObjects.Count);
|
||||
|
||||
// Delete orphaned objects and their data
|
||||
foreach (var fileObject in orphanedObjects)
|
||||
{
|
||||
try
|
||||
{
|
||||
var replicas = await db.FileReplicas
|
||||
.Where(r => r.ObjectId == fileObject.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var replica in replicas.Where(r => r.PoolId.HasValue))
|
||||
{
|
||||
var dest = await fileService.GetRemoteStorageConfig(replica.PoolId!.Value);
|
||||
if (dest == null) continue;
|
||||
var client = FileService.CreateMinioClient(dest);
|
||||
if (client == null) continue;
|
||||
try
|
||||
{
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(replica.StorageId)
|
||||
);
|
||||
if (fileObject.HasCompression)
|
||||
{
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(replica.StorageId + ".compressed")
|
||||
);
|
||||
}
|
||||
if (fileObject.HasThumbnail)
|
||||
{
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(replica.StorageId + ".thumbnail")
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete orphaned file object {ObjectId} from remote storage", fileObject.Id);
|
||||
}
|
||||
}
|
||||
|
||||
db.FileReplicas.RemoveRange(replicas);
|
||||
db.FileObjects.Remove(fileObject);
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Deleted orphaned file object {ObjectId}", fileObject.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to clean up orphaned file object {ObjectId}", fileObject.Id);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Completed file object cleanup job");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
public class FileReanalysisBackgroundService(FileReanalysisService reanalysisService, ILogger<FileReanalysisBackgroundService> logger, IConfiguration config) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("File reanalysis background service started");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await reanalysisService.ProcessNextFileAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error during file reanalysis");
|
||||
}
|
||||
|
||||
// Wait configured milliseconds before processing next file
|
||||
var delayMs = config.GetValue("FileReanalysis:DelayMs", 10000);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(delayMs), stoppingToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("File reanalysis background service stopped");
|
||||
}
|
||||
}
|
||||
577
DysonNetwork.Drive/Storage/FileReanalysisService.cs
Normal file
577
DysonNetwork.Drive/Storage/FileReanalysisService.cs
Normal file
@@ -0,0 +1,577 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using DysonNetwork.Drive.Storage.Options;
|
||||
using FFMpegCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Minio;
|
||||
using Minio.DataModel.Args;
|
||||
using Minio.Exceptions;
|
||||
using NetVips;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
public class FileReanalysisService(
|
||||
AppDatabase db,
|
||||
ILogger<FileReanalysisService> logger,
|
||||
IOptions<FileReanalysisOptions> options)
|
||||
{
|
||||
private readonly FileReanalysisOptions _options = options.Value;
|
||||
private readonly HashSet<string> _failedFileIds = [];
|
||||
private readonly Dictionary<string, HashSet<string>> _bucketObjectCache = new();
|
||||
private int _totalProcessed = 0;
|
||||
private int _reanalysisSuccess = 0;
|
||||
private int _reanalysisFailure = 0;
|
||||
private int _validationCompressionProcessed = 0;
|
||||
private int _validationThumbnailProcessed = 0;
|
||||
|
||||
private async Task<List<SnCloudFile>> GetFilesNeedingReanalysisAsync(int limit = 1000)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var deadline = now.Minus(Duration.FromMinutes(30));
|
||||
return await db.Files
|
||||
.Where(f => f.ObjectId != null)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(f => f.FileReplicas)
|
||||
.Where(f => ((f.Object!.MimeType == null || !f.Object.MimeType.StartsWith("application/")) &&
|
||||
(f.Object!.Meta == null || f.Object.Meta.Count == 0)) || f.Object.Size == 0 ||
|
||||
f.Object.Hash == null)
|
||||
.Where(f => f.Object!.FileReplicas.Count > 0)
|
||||
.Where(f => f.CreatedAt <= deadline)
|
||||
.OrderBy(f => f.Object!.UpdatedAt)
|
||||
.Skip(_failedFileIds.Count)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<SnCloudFile>> GetFilesNeedingCompressionValidationAsync(int offset, int limit = 1000)
|
||||
{
|
||||
return await db.Files
|
||||
.Where(f => f.ObjectId != null)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o!.FileReplicas)
|
||||
.Where(f => f.Object!.HasCompression)
|
||||
.Where(f => f.Object!.FileReplicas.Any(r => r.IsPrimary))
|
||||
.Take(limit)
|
||||
.Skip(offset)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<SnCloudFile>> GetFilesNeedingThumbnailValidationAsync(int offset, int limit = 1000)
|
||||
{
|
||||
return await db.Files
|
||||
.Where(f => f.ObjectId != null)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o!.FileReplicas)
|
||||
.Where(f => f.Object!.HasThumbnail)
|
||||
.Where(f => f.Object!.FileReplicas.Any(r => r.IsPrimary))
|
||||
.Take(limit)
|
||||
.Skip(offset)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> ReanalyzeFileAsync(SnCloudFile file)
|
||||
{
|
||||
logger.LogInformation("Starting reanalysis for file {FileId}: {FileName}", file.Id, file.Name);
|
||||
|
||||
if (file.Object == null)
|
||||
{
|
||||
logger.LogWarning("File {FileId} missing object, skipping reanalysis", file.Id);
|
||||
return true; // not a failure
|
||||
}
|
||||
|
||||
if (file.Object.MimeType != null && file.Object.MimeType.StartsWith("application/") && file.Object.Size != 0 &&
|
||||
file.Object.Hash != null)
|
||||
{
|
||||
logger.LogInformation("File {FileId} already reanalyzed, no need for reanalysis", file.Id);
|
||||
return true; // skip
|
||||
}
|
||||
|
||||
var primaryReplica = file.Object.FileReplicas.FirstOrDefault(r => r.IsPrimary);
|
||||
if (primaryReplica == null)
|
||||
{
|
||||
logger.LogWarning("File {FileId} has no primary replica, skipping reanalysis", file.Id);
|
||||
return true; // not a failure
|
||||
}
|
||||
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"reanalysis_{file.Id}_{Guid.NewGuid()}");
|
||||
try
|
||||
{
|
||||
await DownloadFileAsync(file, primaryReplica, tempPath);
|
||||
|
||||
var fileInfo = new FileInfo(tempPath);
|
||||
var actualSize = fileInfo.Length;
|
||||
var actualHash = await HashFileAsync(tempPath);
|
||||
|
||||
var meta = await ExtractMetadataAsync(file, tempPath);
|
||||
|
||||
if (meta == null && !string.IsNullOrEmpty(file.MimeType) && (file.MimeType.StartsWith("image/") ||
|
||||
file.MimeType.StartsWith("video/") ||
|
||||
file.MimeType.StartsWith("audio/")))
|
||||
{
|
||||
logger.LogWarning("Failed to extract metadata for supported MIME type {MimeType} on file {FileId}",
|
||||
file.MimeType, file.Id);
|
||||
}
|
||||
|
||||
var updated = false;
|
||||
if (file.Object.Size == 0 || file.Object.Size != actualSize)
|
||||
{
|
||||
file.Object.Size = actualSize;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(file.Object.Hash) || file.Object.Hash != actualHash)
|
||||
{
|
||||
file.Object.Hash = actualHash;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (meta is { Count: > 0 })
|
||||
{
|
||||
file.Object.Meta = meta;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated)
|
||||
{
|
||||
db.FileObjects.Update(file.Object);
|
||||
await db.SaveChangesAsync();
|
||||
var metaCount = meta?.Count ?? 0;
|
||||
logger.LogInformation("Successfully reanalyzed file {FileId}, updated metadata with {MetaCount} fields",
|
||||
file.Id, metaCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("File {FileId} already up to date", file.Id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (ObjectNotFoundException)
|
||||
{
|
||||
logger.LogWarning("File {FileId} not found in remote storage, deleting record", file.Id);
|
||||
db.Files.Remove(file);
|
||||
await db.SaveChangesAsync();
|
||||
return true; // handled
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to reanalyze file {FileId}", file.Id);
|
||||
return false; // failure
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateBatchCompressionAndThumbnailAsync(
|
||||
List<SnCloudFile> files,
|
||||
bool validateCompression,
|
||||
bool validateThumbnail
|
||||
)
|
||||
{
|
||||
var poolIds = files.Select(f => f.Object!.FileReplicas.First(r => r.IsPrimary).PoolId)
|
||||
.Where(pid => pid.HasValue)
|
||||
.Select(pid => pid!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var pools = await db.Pools.Where(p => poolIds.Contains(p.Id)).ToDictionaryAsync(p => p.Id);
|
||||
|
||||
var groupedByPool = files.GroupBy(f => f.Object!.FileReplicas.First(r => r.IsPrimary).PoolId);
|
||||
|
||||
foreach (var group in groupedByPool)
|
||||
{
|
||||
if (!group.Key.HasValue) continue;
|
||||
var poolId = group.Key.Value;
|
||||
var poolFiles = group.ToList();
|
||||
|
||||
if (!pools.TryGetValue(poolId, out var pool))
|
||||
{
|
||||
logger.LogWarning("No pool found for pool {PoolId}, skipping batch validation", poolId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var dest = pool.StorageConfig;
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client == null)
|
||||
{
|
||||
logger.LogWarning("Failed to create Minio client for pool {PoolId}, skipping batch validation", poolId);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var file in poolFiles)
|
||||
{
|
||||
if (file.Object == null) continue;
|
||||
var primaryReplica = file.Object.FileReplicas.FirstOrDefault(r => r.IsPrimary);
|
||||
if (primaryReplica == null) continue;
|
||||
|
||||
var baseStorageId = primaryReplica.StorageId;
|
||||
|
||||
if (validateCompression && file.Object.HasCompression)
|
||||
{
|
||||
try
|
||||
{
|
||||
var statArgs = new StatObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(baseStorageId + ".compressed");
|
||||
await client.StatObjectAsync(statArgs);
|
||||
}
|
||||
catch (ObjectNotFoundException)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"File {FileId} has compression flag but compressed version not found, setting HasCompression to false",
|
||||
file.Id);
|
||||
await db.FileObjects
|
||||
.Where(f => f.Id == file.ObjectId!)
|
||||
.ExecuteUpdateAsync(p => p.SetProperty(c => c.HasCompression, false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to stat compressed version for file {FileId}", file.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (validateThumbnail && file.Object.HasThumbnail)
|
||||
{
|
||||
try
|
||||
{
|
||||
var statArgs = new StatObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(baseStorageId + ".thumbnail");
|
||||
await client.StatObjectAsync(statArgs);
|
||||
}
|
||||
catch (ObjectNotFoundException)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"File {FileId} has thumbnail flag but thumbnail not found, setting HasThumbnail to false",
|
||||
file.Id);
|
||||
await db.FileObjects
|
||||
.Where(f => f.Id == file.ObjectId!)
|
||||
.ExecuteUpdateAsync(p => p.SetProperty(c => c.HasThumbnail, false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to stat thumbnail for file {FileId}", file.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessNextFileAsync()
|
||||
{
|
||||
List<SnCloudFile> reanalysisFiles = [];
|
||||
if (_options.Enabled)
|
||||
{
|
||||
reanalysisFiles = await GetFilesNeedingReanalysisAsync(10);
|
||||
reanalysisFiles = reanalysisFiles.Where(f => !_failedFileIds.Contains(f.Id.ToString())).ToList();
|
||||
|
||||
if (reanalysisFiles.Count > 0)
|
||||
{
|
||||
var file = reanalysisFiles[0];
|
||||
var success = await ReanalyzeFileAsync(file);
|
||||
if (!success)
|
||||
{
|
||||
logger.LogWarning("Failed to reanalyze file {FileId}, skipping for now", file.Id);
|
||||
_failedFileIds.Add(file.Id);
|
||||
_reanalysisFailure++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_reanalysisSuccess++;
|
||||
}
|
||||
|
||||
_totalProcessed++;
|
||||
var successRate = (_reanalysisSuccess + _reanalysisFailure) > 0
|
||||
? (double)_reanalysisSuccess / (_reanalysisSuccess + _reanalysisFailure) * 100
|
||||
: 0;
|
||||
logger.LogInformation(
|
||||
"Reanalysis progress: {ReanalysisSuccess} succeeded, {ReanalysisFailure} failed ({SuccessRate:F1}%)",
|
||||
_reanalysisSuccess, _reanalysisFailure, successRate);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("File reanalysis is disabled, skipping reanalysis but continuing with validation");
|
||||
}
|
||||
|
||||
if (_options.ValidateCompression)
|
||||
{
|
||||
var compressionFiles = await GetFilesNeedingCompressionValidationAsync(_validationCompressionProcessed);
|
||||
if (compressionFiles.Count > 0)
|
||||
{
|
||||
await ValidateBatchCompressionAndThumbnailAsync(compressionFiles, true, false);
|
||||
_validationCompressionProcessed += compressionFiles.Count;
|
||||
_totalProcessed += compressionFiles.Count;
|
||||
logger.LogInformation("Batch compression validation progress: {ValidationProcessed} processed",
|
||||
_validationCompressionProcessed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.ValidateThumbnails)
|
||||
{
|
||||
var thumbnailFiles = await GetFilesNeedingThumbnailValidationAsync(_validationThumbnailProcessed);
|
||||
if (thumbnailFiles.Count > 0)
|
||||
{
|
||||
await ValidateBatchCompressionAndThumbnailAsync(thumbnailFiles, false, true);
|
||||
_validationThumbnailProcessed += thumbnailFiles.Count;
|
||||
_totalProcessed += thumbnailFiles.Count;
|
||||
logger.LogInformation("Batch thumbnail validation progress: {ValidationProcessed} processed",
|
||||
_validationThumbnailProcessed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (reanalysisFiles.Count > 0 && !_options.Enabled)
|
||||
{
|
||||
logger.LogInformation("Reanalysis is disabled, no other work to do");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("No files found needing reanalysis or validation");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(SnCloudFile file, SnFileReplica replica, string tempPath)
|
||||
{
|
||||
if (replica.PoolId == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Replica for file {file.Id} has no pool ID");
|
||||
}
|
||||
|
||||
var pool = await db.Pools.FindAsync(replica.PoolId.Value);
|
||||
if (pool == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No remote storage configured for pool {replica.PoolId}");
|
||||
}
|
||||
|
||||
var dest = pool.StorageConfig;
|
||||
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to create Minio client for pool {replica.PoolId}");
|
||||
}
|
||||
|
||||
await using var fileStream = File.Create(tempPath);
|
||||
var getObjectArgs = new GetObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(replica.StorageId)
|
||||
.WithCallbackStream(async (stream, cancellationToken) =>
|
||||
{
|
||||
await stream.CopyToAsync(fileStream, cancellationToken);
|
||||
});
|
||||
|
||||
await client.GetObjectAsync(getObjectArgs);
|
||||
logger.LogDebug("Downloaded file {FileId} to {TempPath}", file.Id, tempPath);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object?>?> ExtractMetadataAsync(SnCloudFile file, string filePath)
|
||||
{
|
||||
var mimeType = file.MimeType;
|
||||
if (string.IsNullOrEmpty(mimeType))
|
||||
{
|
||||
logger.LogWarning("File {FileId} has no MIME type, skipping metadata extraction", file.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (mimeType.Split('/')[0])
|
||||
{
|
||||
case "image":
|
||||
return await ExtractImageMetadataAsync(file, filePath);
|
||||
case "video":
|
||||
case "audio":
|
||||
return await ExtractMediaMetadataAsync(file, filePath);
|
||||
default:
|
||||
logger.LogDebug("Skipping metadata extraction for unsupported MIME type {MimeType} on file {FileId}",
|
||||
mimeType, file.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object?>?> ExtractImageMetadataAsync(SnCloudFile file, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? blurhash = null;
|
||||
try
|
||||
{
|
||||
blurhash = BlurHashSharp.SkiaSharp.BlurHashEncoder.Encode(3, 3, filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to generate blurhash for file {FileId}, skipping", file.Id);
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
stream.Position = 0;
|
||||
|
||||
using var vipsImage = Image.NewFromStream(stream);
|
||||
var width = vipsImage.Width;
|
||||
var height = vipsImage.Height;
|
||||
var orientation = 1;
|
||||
try
|
||||
{
|
||||
orientation = vipsImage.Get("orientation") as int? ?? 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
var meta = new Dictionary<string, object?>
|
||||
{
|
||||
["format"] = vipsImage.Get("vips-loader") ?? "unknown",
|
||||
["width"] = width,
|
||||
["height"] = height,
|
||||
["orientation"] = orientation,
|
||||
};
|
||||
|
||||
if (blurhash != null)
|
||||
{
|
||||
meta["blurhash"] = blurhash;
|
||||
}
|
||||
|
||||
var exif = new Dictionary<string, object>();
|
||||
|
||||
foreach (var field in vipsImage.GetFields())
|
||||
{
|
||||
if (IsIgnoredField(field)) continue;
|
||||
var value = vipsImage.Get(field);
|
||||
if (field.StartsWith("exif-"))
|
||||
exif[field.Replace("exif-", "")] = value;
|
||||
else
|
||||
meta[field] = value;
|
||||
}
|
||||
|
||||
if (orientation is 6 or 8) (width, height) = (height, width);
|
||||
meta["exif"] = exif;
|
||||
meta["ratio"] = height != 0 ? (double)width / height : 0;
|
||||
return meta;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object?>?> ExtractMediaMetadataAsync(SnCloudFile file, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mediaInfo = await FFProbe.AnalyseAsync(filePath);
|
||||
var meta = new Dictionary<string, object?>
|
||||
{
|
||||
["width"] = mediaInfo.PrimaryVideoStream?.Width,
|
||||
["height"] = mediaInfo.PrimaryVideoStream?.Height,
|
||||
["duration"] = mediaInfo.Duration.TotalSeconds,
|
||||
["format_name"] = mediaInfo.Format.FormatName,
|
||||
["format_long_name"] = mediaInfo.Format.FormatLongName,
|
||||
["start_time"] = mediaInfo.Format.StartTime.ToString(),
|
||||
["bit_rate"] = mediaInfo.Format.BitRate.ToString(CultureInfo.InvariantCulture),
|
||||
["tags"] = mediaInfo.Format.Tags ?? new Dictionary<string, string>(),
|
||||
["chapters"] = mediaInfo.Chapters,
|
||||
["video_streams"] = mediaInfo.VideoStreams.Select(s => new
|
||||
{
|
||||
s.AvgFrameRate,
|
||||
s.BitRate,
|
||||
s.CodecName,
|
||||
s.Duration,
|
||||
s.Height,
|
||||
s.Width,
|
||||
s.Language,
|
||||
s.PixelFormat,
|
||||
s.Rotation
|
||||
}).Where(s => double.IsNormal(s.AvgFrameRate)).ToList(),
|
||||
["audio_streams"] = mediaInfo.AudioStreams.Select(s => new
|
||||
{
|
||||
s.BitRate,
|
||||
s.Channels,
|
||||
s.ChannelLayout,
|
||||
s.CodecName,
|
||||
s.Duration,
|
||||
s.Language,
|
||||
s.SampleRateHz
|
||||
})
|
||||
.ToList(),
|
||||
};
|
||||
if (mediaInfo.PrimaryVideoStream is not null)
|
||||
meta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width /
|
||||
mediaInfo.PrimaryVideoStream.Height;
|
||||
return meta;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to analyze media file {FileId}", file.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> HashFileAsync(string filePath, int chunkSize = 1024 * 1024)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (fileInfo.Length > chunkSize * 1024 * 5)
|
||||
return await HashFastApproximateAsync(filePath, chunkSize);
|
||||
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
using var md5 = MD5.Create();
|
||||
var hashBytes = await md5.ComputeHashAsync(stream);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static async Task<string> HashFastApproximateAsync(string filePath, int chunkSize = 1024 * 1024)
|
||||
{
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
|
||||
var buffer = new byte[chunkSize * 2];
|
||||
var fileLength = stream.Length;
|
||||
|
||||
var bytesRead = await stream.ReadAsync(buffer.AsMemory(0, chunkSize));
|
||||
|
||||
if (fileLength > chunkSize)
|
||||
{
|
||||
stream.Seek(-chunkSize, SeekOrigin.End);
|
||||
bytesRead += await stream.ReadAsync(buffer.AsMemory(chunkSize, chunkSize));
|
||||
}
|
||||
|
||||
var hash = MD5.HashData(buffer.AsSpan(0, bytesRead));
|
||||
stream.Position = 0;
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool IsIgnoredField(string fieldName)
|
||||
{
|
||||
var gpsFields = new[]
|
||||
{
|
||||
"gps-latitude", "gps-longitude", "gps-altitude", "gps-latitude-ref", "gps-longitude-ref",
|
||||
"gps-altitude-ref", "gps-timestamp", "gps-datestamp", "gps-speed", "gps-speed-ref", "gps-track",
|
||||
"gps-track-ref", "gps-img-direction", "gps-img-direction-ref", "gps-dest-latitude",
|
||||
"gps-dest-longitude", "gps-dest-latitude-ref", "gps-dest-longitude-ref", "gps-processing-method",
|
||||
"gps-area-information"
|
||||
};
|
||||
|
||||
if (fieldName.StartsWith("exif-GPS")) return true;
|
||||
if (fieldName.StartsWith("ifd3-GPS")) return true;
|
||||
if (fieldName.EndsWith("-data")) return true;
|
||||
return gpsFields.Any(gpsField => fieldName.StartsWith(gpsField, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
|
||||
{
|
||||
var client = new MinioClient()
|
||||
.WithEndpoint(dest.Endpoint)
|
||||
.WithRegion(dest.Region)
|
||||
.WithCredentials(dest.SecretId, dest.SecretKey);
|
||||
if (dest.EnableSsl) client = client.WithSSL();
|
||||
|
||||
return client.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,524 +0,0 @@
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache)
|
||||
{
|
||||
private const string CacheKeyPrefix = "file:ref:";
|
||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new reference to a file for a specific resource
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file to reference</param>
|
||||
/// <param name="usage">The usage context (e.g., "avatar", "post-attachment")</param>
|
||||
/// <param name="resourceId">The ID of the resource using the file</param>
|
||||
/// <param name="expiredAt">Optional expiration time for the file</param>
|
||||
/// <param name="duration">Optional duration after which the file expires (alternative to expiredAt)</param>
|
||||
/// <returns>The created file reference</returns>
|
||||
public async Task<SnCloudFileReference> CreateReferenceAsync(
|
||||
string fileId,
|
||||
string usage,
|
||||
string resourceId,
|
||||
Instant? expiredAt = null,
|
||||
Duration? duration = null
|
||||
)
|
||||
{
|
||||
// Calculate expiration time if needed
|
||||
var finalExpiration = expiredAt;
|
||||
if (duration.HasValue)
|
||||
finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value;
|
||||
|
||||
var reference = new SnCloudFileReference
|
||||
{
|
||||
FileId = fileId,
|
||||
Usage = usage,
|
||||
ResourceId = resourceId,
|
||||
ExpiredAt = finalExpiration
|
||||
};
|
||||
|
||||
db.FileReferences.Add(reference);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await fileService._PurgeCacheAsync(fileId);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task<List<SnCloudFileReference>> CreateReferencesAsync(
|
||||
List<string> fileId,
|
||||
string usage,
|
||||
string resourceId,
|
||||
Instant? expiredAt = null,
|
||||
Duration? duration = null
|
||||
)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var data = fileId.Select(id => new SnCloudFileReference
|
||||
{
|
||||
FileId = id,
|
||||
Usage = usage,
|
||||
ResourceId = resourceId,
|
||||
ExpiredAt = expiredAt ?? now + duration,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
}).ToList();
|
||||
await db.BulkInsertAsync(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all references to a file
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file</param>
|
||||
/// <returns>A list of all references to the file</returns>
|
||||
public async Task<List<SnCloudFileReference>> GetReferencesAsync(string fileId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}list:{fileId}";
|
||||
|
||||
var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
|
||||
if (cachedReferences is not null)
|
||||
return cachedReferences;
|
||||
|
||||
var references = await db.FileReferences
|
||||
.Where(r => r.FileId == fileId)
|
||||
.ToListAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, references, CacheDuration);
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, List<SnCloudFileReference>>> GetReferencesAsync(IEnumerable<string> fileIds)
|
||||
{
|
||||
var fileIdList = fileIds.ToList();
|
||||
var result = new Dictionary<string, List<SnCloudFileReference>>();
|
||||
|
||||
// Check cache for each file ID
|
||||
var uncachedFileIds = new List<string>();
|
||||
foreach (var fileId in fileIdList)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}list:{fileId}";
|
||||
var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
|
||||
if (cachedReferences is not null)
|
||||
{
|
||||
result[fileId] = cachedReferences;
|
||||
}
|
||||
else
|
||||
{
|
||||
uncachedFileIds.Add(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch uncached references from database
|
||||
if (uncachedFileIds.Any())
|
||||
{
|
||||
var dbReferences = await db.FileReferences
|
||||
.Where(r => uncachedFileIds.Contains(r.FileId))
|
||||
.GroupBy(r => r.FileId)
|
||||
.ToDictionaryAsync(r => r.Key, r => r.ToList());
|
||||
|
||||
// Cache the results
|
||||
foreach (var kvp in dbReferences)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}list:{kvp.Key}";
|
||||
await cache.SetAsync(cacheKey, kvp.Value, CacheDuration);
|
||||
result[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of references to a file
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file</param>
|
||||
/// <returns>The number of references to the file</returns>
|
||||
public async Task<int> GetReferenceCountAsync(string fileId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}count:{fileId}";
|
||||
|
||||
var cachedCount = await cache.GetAsync<int?>(cacheKey);
|
||||
if (cachedCount.HasValue)
|
||||
return cachedCount.Value;
|
||||
|
||||
var count = await db.FileReferences
|
||||
.Where(r => r.FileId == fileId)
|
||||
.CountAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, count, CacheDuration);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all references for a specific resource
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The ID of the resource</param>
|
||||
/// <returns>A list of file references associated with the resource</returns>
|
||||
public async Task<List<SnCloudFileReference>> GetResourceReferencesAsync(string resourceId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
|
||||
|
||||
var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
|
||||
if (cachedReferences is not null)
|
||||
return cachedReferences;
|
||||
|
||||
var references = await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId)
|
||||
.ToListAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, references, CacheDuration);
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all file references for a specific usage context
|
||||
/// </summary>
|
||||
/// <param name="usage">The usage context</param>
|
||||
/// <returns>A list of file references with the specified usage</returns>
|
||||
public async Task<List<SnCloudFileReference>> GetUsageReferencesAsync(string usage)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}usage:{usage}";
|
||||
|
||||
var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
|
||||
if (cachedReferences is not null)
|
||||
return cachedReferences;
|
||||
|
||||
var references = await db.FileReferences
|
||||
.Where(r => r.Usage == usage)
|
||||
.ToListAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, references, CacheDuration);
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes references for a specific resource
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The ID of the resource</param>
|
||||
/// <returns>The number of deleted references</returns>
|
||||
public async Task<int> DeleteResourceReferencesAsync(string resourceId)
|
||||
{
|
||||
var references = await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId)
|
||||
.ToListAsync();
|
||||
|
||||
var fileIds = references.Select(r => r.FileId).Distinct().ToList();
|
||||
|
||||
db.FileReferences.RemoveRange(references);
|
||||
var deletedCount = await db.SaveChangesAsync();
|
||||
|
||||
// Purge caches
|
||||
var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
|
||||
tasks.Add(PurgeCacheForResourceAsync(resourceId));
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes references for a specific resource and usage
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The ID of the resource</param>
|
||||
/// <param name="usage">The usage context</param>
|
||||
/// <returns>The number of deleted references</returns>
|
||||
public async Task<int> DeleteResourceReferencesAsync(string resourceId, string usage)
|
||||
{
|
||||
var references = await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId && r.Usage == usage)
|
||||
.ToListAsync();
|
||||
|
||||
if (references.Count == 0)
|
||||
return 0;
|
||||
|
||||
var fileIds = references.Select(r => r.FileId).Distinct().ToList();
|
||||
|
||||
db.FileReferences.RemoveRange(references);
|
||||
var deletedCount = await db.SaveChangesAsync();
|
||||
|
||||
// Purge caches
|
||||
var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
|
||||
tasks.Add(PurgeCacheForResourceAsync(resourceId));
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteResourceReferencesBatchAsync(IEnumerable<string> resourceIds, string? usage = null)
|
||||
{
|
||||
var resourceIdList = resourceIds.ToList();
|
||||
var references = await db.FileReferences
|
||||
.Where(r => resourceIdList.Contains(r.ResourceId))
|
||||
.If(usage != null, q => q.Where(q => q.Usage == usage))
|
||||
.ToListAsync();
|
||||
|
||||
if (references.Count == 0)
|
||||
return 0;
|
||||
|
||||
var fileIds = references.Select(r => r.FileId).Distinct().ToList();
|
||||
|
||||
db.FileReferences.RemoveRange(references);
|
||||
var deletedCount = await db.SaveChangesAsync();
|
||||
|
||||
// Purge caches for files and resources
|
||||
var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
|
||||
tasks.AddRange(resourceIdList.Select(PurgeCacheForResourceAsync));
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a specific file reference
|
||||
/// </summary>
|
||||
/// <param name="referenceId">The ID of the reference to delete</param>
|
||||
/// <returns>True if the reference was deleted, false otherwise</returns>
|
||||
public async Task<bool> DeleteReferenceAsync(Guid referenceId)
|
||||
{
|
||||
var reference = await db.FileReferences
|
||||
.FirstOrDefaultAsync(r => r.Id == referenceId);
|
||||
|
||||
if (reference == null)
|
||||
return false;
|
||||
|
||||
db.FileReferences.Remove(reference);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Purge caches
|
||||
await fileService._PurgeCacheAsync(reference.FileId);
|
||||
await PurgeCacheForResourceAsync(reference.ResourceId);
|
||||
await PurgeCacheForFileAsync(reference.FileId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the files referenced by a resource
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The ID of the resource</param>
|
||||
/// <param name="newFileIds">The new list of file IDs</param>
|
||||
/// <param name="usage">The usage context</param>
|
||||
/// <param name="expiredAt">Optional expiration time for newly added files</param>
|
||||
/// <param name="duration">Optional duration after which newly added files expire</param>
|
||||
/// <returns>A list of the updated file references</returns>
|
||||
public async Task<List<SnCloudFileReference>> UpdateResourceFilesAsync(
|
||||
string resourceId,
|
||||
IEnumerable<string>? newFileIds,
|
||||
string usage,
|
||||
Instant? expiredAt = null,
|
||||
Duration? duration = null)
|
||||
{
|
||||
if (newFileIds == null)
|
||||
return new List<SnCloudFileReference>();
|
||||
|
||||
var existingReferences = await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId && r.Usage == usage)
|
||||
.ToListAsync();
|
||||
|
||||
var existingFileIds = existingReferences.Select(r => r.FileId).ToHashSet();
|
||||
var newFileIdsList = newFileIds.ToList();
|
||||
var newFileIdsSet = newFileIdsList.ToHashSet();
|
||||
|
||||
// Files to remove
|
||||
var toRemove = existingReferences
|
||||
.Where(r => !newFileIdsSet.Contains(r.FileId))
|
||||
.ToList();
|
||||
|
||||
// Files to add
|
||||
var toAdd = newFileIdsList
|
||||
.Where(id => !existingFileIds.Contains(id))
|
||||
.Select(id => new SnCloudFileReference
|
||||
{
|
||||
FileId = id,
|
||||
Usage = usage,
|
||||
ResourceId = resourceId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Apply changes
|
||||
if (toRemove.Any())
|
||||
db.FileReferences.RemoveRange(toRemove);
|
||||
|
||||
if (toAdd.Any())
|
||||
db.FileReferences.AddRange(toAdd);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Update expiration for newly added references if specified
|
||||
if ((expiredAt.HasValue || duration.HasValue) && toAdd.Any())
|
||||
{
|
||||
var finalExpiration = expiredAt;
|
||||
if (duration.HasValue)
|
||||
{
|
||||
finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value;
|
||||
}
|
||||
|
||||
// Update newly added references with the expiration time
|
||||
var referenceIds = await db.FileReferences
|
||||
.Where(r => toAdd.Select(a => a.FileId).Contains(r.FileId) &&
|
||||
r.ResourceId == resourceId &&
|
||||
r.Usage == usage)
|
||||
.Select(r => r.Id)
|
||||
.ToListAsync();
|
||||
|
||||
await db.FileReferences
|
||||
.Where(r => referenceIds.Contains(r.Id))
|
||||
.ExecuteUpdateAsync(setter => setter.SetProperty(
|
||||
r => r.ExpiredAt,
|
||||
_ => finalExpiration
|
||||
));
|
||||
}
|
||||
|
||||
// Purge caches
|
||||
var allFileIds = existingFileIds.Union(newFileIdsSet).ToList();
|
||||
var tasks = allFileIds.Select(fileService._PurgeCacheAsync).ToList();
|
||||
tasks.Add(PurgeCacheForResourceAsync(resourceId));
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Return updated references
|
||||
return await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId && r.Usage == usage)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files referenced by a resource
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The ID of the resource</param>
|
||||
/// <param name="usage">Optional filter by usage context</param>
|
||||
/// <returns>A list of files referenced by the resource</returns>
|
||||
public async Task<List<SnCloudFile>> GetResourceFilesAsync(string resourceId, string? usage = null)
|
||||
{
|
||||
var query = db.FileReferences.Where(r => r.ResourceId == resourceId);
|
||||
|
||||
if (usage != null)
|
||||
query = query.Where(r => r.Usage == usage);
|
||||
|
||||
var references = await query.ToListAsync();
|
||||
var fileIds = references.Select(r => r.FileId).ToList();
|
||||
|
||||
return await db.Files
|
||||
.Where(f => fileIds.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purges all caches related to a resource
|
||||
/// </summary>
|
||||
private async Task PurgeCacheForResourceAsync(string resourceId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
|
||||
await cache.RemoveAsync(cacheKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purges all caches related to a file
|
||||
/// </summary>
|
||||
private async Task PurgeCacheForFileAsync(string fileId)
|
||||
{
|
||||
var cacheKeys = new[]
|
||||
{
|
||||
$"{CacheKeyPrefix}list:{fileId}",
|
||||
$"{CacheKeyPrefix}count:{fileId}"
|
||||
};
|
||||
|
||||
var tasks = cacheKeys.Select(cache.RemoveAsync);
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the expiration time for a file reference
|
||||
/// </summary>
|
||||
/// <param name="referenceId">The ID of the reference</param>
|
||||
/// <param name="expiredAt">The new expiration time, or null to remove expiration</param>
|
||||
/// <returns>True if the reference was found and updated, false otherwise</returns>
|
||||
public async Task<bool> SetReferenceExpirationAsync(Guid referenceId, Instant? expiredAt)
|
||||
{
|
||||
var reference = await db.FileReferences
|
||||
.FirstOrDefaultAsync(r => r.Id == referenceId);
|
||||
|
||||
if (reference == null)
|
||||
return false;
|
||||
|
||||
reference.ExpiredAt = expiredAt;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await PurgeCacheForFileAsync(reference.FileId);
|
||||
await PurgeCacheForResourceAsync(reference.ResourceId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the expiration time for all references to a file
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file</param>
|
||||
/// <param name="expiredAt">The new expiration time, or null to remove expiration</param>
|
||||
/// <returns>The number of references updated</returns>
|
||||
public async Task<int> SetFileReferencesExpirationAsync(string fileId, Instant? expiredAt)
|
||||
{
|
||||
var rowsAffected = await db.FileReferences
|
||||
.Where(r => r.FileId == fileId)
|
||||
.ExecuteUpdateAsync(setter => setter.SetProperty(
|
||||
r => r.ExpiredAt,
|
||||
_ => expiredAt
|
||||
));
|
||||
|
||||
if (rowsAffected > 0)
|
||||
{
|
||||
await fileService._PurgeCacheAsync(fileId);
|
||||
await PurgeCacheForFileAsync(fileId);
|
||||
}
|
||||
|
||||
return rowsAffected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all file references for a specific resource and usage type
|
||||
/// </summary>
|
||||
/// <param name="resourceId">The resource ID</param>
|
||||
/// <param name="usageType">The usage type</param>
|
||||
/// <returns>List of file references</returns>
|
||||
public async Task<List<SnCloudFileReference>> GetResourceReferencesAsync(string resourceId, string usageType)
|
||||
{
|
||||
return await db.FileReferences
|
||||
.Where(r => r.ResourceId == resourceId && r.Usage == usageType)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a file has any references
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID to check</param>
|
||||
/// <returns>True if the file has references, false otherwise</returns>
|
||||
public async Task<bool> HasFileReferencesAsync(string fileId)
|
||||
{
|
||||
return await db.FileReferences.AnyAsync(r => r.FileId == fileId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the expiration time for a file reference using a duration from now
|
||||
/// </summary>
|
||||
/// <param name="referenceId">The ID of the reference</param>
|
||||
/// <param name="duration">The duration after which the reference expires, or null to remove expiration</param>
|
||||
/// <returns>True if the reference was found and updated, false otherwise</returns>
|
||||
public async Task<bool> SetReferenceExpirationDurationAsync(Guid referenceId, Duration? duration)
|
||||
{
|
||||
Instant? expiredAt = null;
|
||||
if (duration.HasValue)
|
||||
{
|
||||
expiredAt = SystemClock.Instance.GetCurrentInstant() + duration.Value;
|
||||
}
|
||||
|
||||
return await SetReferenceExpirationAsync(referenceId, expiredAt);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using NodaTime;
|
||||
using Duration = NodaTime.Duration;
|
||||
|
||||
namespace DysonNetwork.Drive.Storage;
|
||||
|
||||
public class FileReferenceServiceGrpc(FileReferenceService fileReferenceService)
|
||||
: Shared.Proto.FileReferenceService.FileReferenceServiceBase
|
||||
{
|
||||
public override async Task<Shared.Proto.CloudFileReference> CreateReference(CreateReferenceRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
Instant? expiredAt = null;
|
||||
if (request.ExpiredAt != null)
|
||||
expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds);
|
||||
else if (request.Duration != null)
|
||||
expiredAt = SystemClock.Instance.GetCurrentInstant() +
|
||||
Duration.FromTimeSpan(request.Duration.ToTimeSpan());
|
||||
|
||||
var reference = await fileReferenceService.CreateReferenceAsync(
|
||||
request.FileId,
|
||||
request.Usage,
|
||||
request.ResourceId,
|
||||
expiredAt
|
||||
);
|
||||
return reference.ToProtoValue();
|
||||
}
|
||||
|
||||
public override async Task<CreateReferenceBatchResponse> CreateReferenceBatch(CreateReferenceBatchRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
Instant? expiredAt = null;
|
||||
if (request.ExpiredAt != null)
|
||||
expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds);
|
||||
else if (request.Duration != null)
|
||||
expiredAt = SystemClock.Instance.GetCurrentInstant() +
|
||||
Duration.FromTimeSpan(request.Duration.ToTimeSpan());
|
||||
|
||||
var references = await fileReferenceService.CreateReferencesAsync(
|
||||
request.FilesId.ToList(),
|
||||
request.Usage,
|
||||
request.ResourceId,
|
||||
expiredAt
|
||||
);
|
||||
var response = new CreateReferenceBatchResponse();
|
||||
response.References.AddRange(references.Select(r => r.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetReferencesResponse> GetReferences(GetReferencesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var references = await fileReferenceService.GetReferencesAsync(request.FileId);
|
||||
var response = new GetReferencesResponse();
|
||||
response.References.AddRange(references.Select(r => r.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetReferenceCountResponse> GetReferenceCount(GetReferenceCountRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var count = await fileReferenceService.GetReferenceCountAsync(request.FileId);
|
||||
return new GetReferenceCountResponse { Count = count };
|
||||
}
|
||||
|
||||
public override async Task<GetReferencesResponse> GetResourceReferences(GetResourceReferencesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var references = await fileReferenceService.GetResourceReferencesAsync(request.ResourceId, request.Usage);
|
||||
var response = new GetReferencesResponse();
|
||||
response.References.AddRange(references.Select(r => r.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetResourceFilesResponse> GetResourceFiles(GetResourceFilesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var files = await fileReferenceService.GetResourceFilesAsync(request.ResourceId, request.Usage);
|
||||
var response = new GetResourceFilesResponse();
|
||||
response.Files.AddRange(files.Select(f => f.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<DeleteResourceReferencesResponse> DeleteResourceReferences(
|
||||
DeleteResourceReferencesRequest request, ServerCallContext context)
|
||||
{
|
||||
int deletedCount;
|
||||
if (request.Usage is null)
|
||||
deletedCount = await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId);
|
||||
else
|
||||
deletedCount =
|
||||
await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId, request.Usage!);
|
||||
return new DeleteResourceReferencesResponse { DeletedCount = deletedCount };
|
||||
}
|
||||
|
||||
public override async Task<DeleteResourceReferencesResponse> DeleteResourceReferencesBatch(DeleteResourceReferencesBatchRequest request, ServerCallContext context)
|
||||
{
|
||||
var resourceIds = request.ResourceIds.ToList();
|
||||
int deletedCount;
|
||||
if (request.Usage is null)
|
||||
deletedCount = await fileReferenceService.DeleteResourceReferencesBatchAsync(resourceIds);
|
||||
else
|
||||
deletedCount =
|
||||
await fileReferenceService.DeleteResourceReferencesBatchAsync(resourceIds, request.Usage!);
|
||||
return new DeleteResourceReferencesResponse { DeletedCount = deletedCount };
|
||||
}
|
||||
|
||||
public override async Task<DeleteReferenceResponse> DeleteReference(DeleteReferenceRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var success = await fileReferenceService.DeleteReferenceAsync(Guid.Parse(request.ReferenceId));
|
||||
return new DeleteReferenceResponse { Success = success };
|
||||
}
|
||||
|
||||
public override async Task<UpdateResourceFilesResponse> UpdateResourceFiles(UpdateResourceFilesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
Instant? expiredAt = null;
|
||||
if (request.ExpiredAt != null)
|
||||
{
|
||||
expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds);
|
||||
}
|
||||
else if (request.Duration != null)
|
||||
{
|
||||
expiredAt = SystemClock.Instance.GetCurrentInstant() +
|
||||
Duration.FromTimeSpan(request.Duration.ToTimeSpan());
|
||||
}
|
||||
|
||||
var references = await fileReferenceService.UpdateResourceFilesAsync(
|
||||
request.ResourceId,
|
||||
request.FileIds,
|
||||
request.Usage,
|
||||
expiredAt
|
||||
);
|
||||
var response = new UpdateResourceFilesResponse();
|
||||
response.References.AddRange(references.Select(r => r.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<SetReferenceExpirationResponse> SetReferenceExpiration(
|
||||
SetReferenceExpirationRequest request, ServerCallContext context)
|
||||
{
|
||||
Instant? expiredAt = null;
|
||||
if (request.ExpiredAt != null)
|
||||
{
|
||||
expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds);
|
||||
}
|
||||
else if (request.Duration != null)
|
||||
{
|
||||
expiredAt = SystemClock.Instance.GetCurrentInstant() +
|
||||
Duration.FromTimeSpan(request.Duration.ToTimeSpan());
|
||||
}
|
||||
|
||||
var success =
|
||||
await fileReferenceService.SetReferenceExpirationAsync(Guid.Parse(request.ReferenceId), expiredAt);
|
||||
return new SetReferenceExpirationResponse { Success = success };
|
||||
}
|
||||
|
||||
public override async Task<SetFileReferencesExpirationResponse> SetFileReferencesExpiration(
|
||||
SetFileReferencesExpirationRequest request, ServerCallContext context)
|
||||
{
|
||||
var expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds);
|
||||
var updatedCount = await fileReferenceService.SetFileReferencesExpirationAsync(request.FileId, expiredAt);
|
||||
return new SetFileReferencesExpirationResponse { UpdatedCount = updatedCount };
|
||||
}
|
||||
|
||||
public override async Task<HasFileReferencesResponse> HasFileReferences(HasFileReferencesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var hasReferences = await fileReferenceService.HasFileReferencesAsync(request.FileId);
|
||||
return new HasFileReferencesResponse { HasReferences = hasReferences };
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class FileService(
|
||||
|
||||
public async Task<SnCloudFile?> GetFileAsync(string fileId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{fileId}";
|
||||
var cacheKey = string.Concat(CacheKeyPrefix, fileId);
|
||||
|
||||
var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey);
|
||||
if (cachedFile is not null)
|
||||
@@ -38,8 +38,9 @@ public class FileService(
|
||||
|
||||
var file = await db.Files
|
||||
.Where(f => f.Id == fileId)
|
||||
.Include(f => f.Pool)
|
||||
.Include(f => f.Bundle)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o.FileReplicas)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (file != null)
|
||||
@@ -55,7 +56,7 @@ public class FileService(
|
||||
|
||||
foreach (var fileId in fileIds)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{fileId}";
|
||||
var cacheKey = string.Concat(CacheKeyPrefix, fileId);
|
||||
var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey);
|
||||
|
||||
if (cachedFile != null)
|
||||
@@ -68,12 +69,14 @@ public class FileService(
|
||||
{
|
||||
var dbFiles = await db.Files
|
||||
.Where(f => uncachedIds.Contains(f.Id))
|
||||
.Include(f => f.Pool)
|
||||
.Include(f => f.Bundle)
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o.FileReplicas)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var file in dbFiles)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{file.Id}";
|
||||
var cacheKey = string.Concat(CacheKeyPrefix, file.Id);
|
||||
await cache.SetAsync(cacheKey, file, CacheDuration);
|
||||
cachedFiles[file.Id] = file;
|
||||
}
|
||||
@@ -106,7 +109,9 @@ public class FileService(
|
||||
var (managedTempPath, fileSize, finalContentType) =
|
||||
await PrepareFileAsync(fileId, filePath, fileName, contentType);
|
||||
|
||||
var file = CreateFileObject(fileId, fileName, finalContentType, fileSize, finalExpiredAt, bundle, accountId);
|
||||
var fileObject = CreateFileObject(fileId, accountId, finalContentType, fileSize);
|
||||
|
||||
var file = CreateCloudFile(fileId, fileName, fileObject, finalExpiredAt, bundle, accountId);
|
||||
|
||||
if (!pool.PolicyConfig.NoMetadata)
|
||||
{
|
||||
@@ -114,11 +119,11 @@ public class FileService(
|
||||
}
|
||||
|
||||
var (processingPath, isTempFile) =
|
||||
await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, file);
|
||||
await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, fileObject);
|
||||
|
||||
file.Hash = await HashFileAsync(processingPath);
|
||||
fileObject.Hash = await HashFileAsync(processingPath);
|
||||
|
||||
await SaveFileToDatabaseAsync(file);
|
||||
await SaveFileToDatabaseAsync(file, fileObject, pool.Id);
|
||||
|
||||
await PublishFileUploadedEventAsync(file, pool, processingPath, isTempFile);
|
||||
|
||||
@@ -178,11 +183,25 @@ public class FileService(
|
||||
return (managedTempPath, fileSize, finalContentType);
|
||||
}
|
||||
|
||||
private SnCloudFile CreateFileObject(
|
||||
private SnFileObject CreateFileObject(
|
||||
string fileId,
|
||||
Guid accountId,
|
||||
string contentType,
|
||||
long fileSize
|
||||
)
|
||||
{
|
||||
return new SnFileObject
|
||||
{
|
||||
Id = fileId,
|
||||
MimeType = contentType,
|
||||
Size = fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
private SnCloudFile CreateCloudFile(
|
||||
string fileId,
|
||||
string fileName,
|
||||
string contentType,
|
||||
long fileSize,
|
||||
SnFileObject fileObject,
|
||||
Instant? expiredAt,
|
||||
SnFileBundle? bundle,
|
||||
Guid accountId
|
||||
@@ -192,24 +211,24 @@ public class FileService(
|
||||
{
|
||||
Id = fileId,
|
||||
Name = fileName,
|
||||
MimeType = contentType,
|
||||
Size = fileSize,
|
||||
Object = fileObject,
|
||||
ObjectId = fileId,
|
||||
ExpiredAt = expiredAt,
|
||||
BundleId = bundle?.Id,
|
||||
AccountId = accountId,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(string processingPath, bool isTempFile)> ProcessEncryptionAsync(
|
||||
private Task<(string processingPath, bool isTempFile)> ProcessEncryptionAsync(
|
||||
string fileId,
|
||||
string managedTempPath,
|
||||
string? encryptPassword,
|
||||
FilePool pool,
|
||||
SnCloudFile file
|
||||
SnFileObject fileObject
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(encryptPassword))
|
||||
return (managedTempPath, true);
|
||||
return Task.FromResult((managedTempPath, true));
|
||||
|
||||
if (!pool.PolicyConfig.AllowEncryption)
|
||||
throw new InvalidOperationException("Encryption is not allowed in this pool");
|
||||
@@ -219,17 +238,31 @@ public class FileService(
|
||||
|
||||
File.Delete(managedTempPath);
|
||||
|
||||
file.IsEncrypted = true;
|
||||
file.MimeType = "application/octet-stream";
|
||||
file.Size = new FileInfo(encryptedPath).Length;
|
||||
fileObject.MimeType = "application/octet-stream";
|
||||
fileObject.Size = new FileInfo(encryptedPath).Length;
|
||||
|
||||
return (encryptedPath, true);
|
||||
return Task.FromResult((encryptedPath, true));
|
||||
}
|
||||
|
||||
private async Task SaveFileToDatabaseAsync(SnCloudFile file)
|
||||
private async Task SaveFileToDatabaseAsync(SnCloudFile file, SnFileObject fileObject, Guid poolId)
|
||||
{
|
||||
var replica = new SnFileReplica
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ObjectId = file.Id,
|
||||
PoolId = poolId,
|
||||
StorageId = file.StorageId ?? file.Id,
|
||||
Status = SnFileReplicaStatus.Available,
|
||||
IsPrimary = true
|
||||
};
|
||||
|
||||
|
||||
db.Files.Add(file);
|
||||
db.FileObjects.Add(fileObject);
|
||||
db.FileReplicas.Add(replica);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
file.ObjectId = file.Id;
|
||||
file.StorageId ??= file.Id;
|
||||
}
|
||||
|
||||
@@ -252,6 +285,8 @@ public class FileService(
|
||||
|
||||
private async Task ExtractMetadataAsync(SnCloudFile file, string filePath)
|
||||
{
|
||||
if (file.Object == null) return;
|
||||
|
||||
switch (file.MimeType?.Split('/')[0])
|
||||
{
|
||||
case "image":
|
||||
@@ -297,11 +332,11 @@ public class FileService(
|
||||
if (orientation is 6 or 8) (width, height) = (height, width);
|
||||
meta["exif"] = exif;
|
||||
meta["ratio"] = height != 0 ? (double)width / height : 0;
|
||||
file.FileMeta = meta;
|
||||
file.Object.Meta = meta;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
file.FileMeta = new Dictionary<string, object?>();
|
||||
file.Object.Meta = new Dictionary<string, object?>();
|
||||
logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id);
|
||||
}
|
||||
|
||||
@@ -312,7 +347,7 @@ public class FileService(
|
||||
try
|
||||
{
|
||||
var mediaInfo = await FFProbe.AnalyseAsync(filePath);
|
||||
file.FileMeta = new Dictionary<string, object?>
|
||||
file.Object.Meta = new Dictionary<string, object?>
|
||||
{
|
||||
["width"] = mediaInfo.PrimaryVideoStream?.Width,
|
||||
["height"] = mediaInfo.PrimaryVideoStream?.Height,
|
||||
@@ -348,8 +383,8 @@ public class FileService(
|
||||
.ToList(),
|
||||
};
|
||||
if (mediaInfo.PrimaryVideoStream is not null)
|
||||
file.FileMeta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width /
|
||||
mediaInfo.PrimaryVideoStream.Height;
|
||||
file.Object.Meta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width /
|
||||
mediaInfo.PrimaryVideoStream.Height;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -470,8 +505,20 @@ public class FileService(
|
||||
|
||||
await db.Files.Where(f => f.Id == file.Id).ExecuteUpdateAsync(updatable.ToSetPropertyCalls());
|
||||
|
||||
if (updateMask.Paths.Contains("file_meta"))
|
||||
{
|
||||
await db.FileObjects
|
||||
.Where(fo => fo.Id == file.ObjectId)
|
||||
.ExecuteUpdateAsync(setter => setter
|
||||
.SetProperty(fo => fo.Meta, file.FileMeta));
|
||||
}
|
||||
|
||||
await _PurgeCacheAsync(file.Id);
|
||||
return await db.Files.AsNoTracking().FirstAsync(f => f.Id == file.Id);
|
||||
return await db.Files
|
||||
.AsNoTracking()
|
||||
.Include(f => f.Object)
|
||||
.ThenInclude(o => o.FileReplicas)
|
||||
.FirstAsync(f => f.Id == file.Id);
|
||||
}
|
||||
|
||||
public async Task DeleteFileAsync(SnCloudFile file, bool skipData = false)
|
||||
@@ -481,17 +528,46 @@ public class FileService(
|
||||
await _PurgeCacheAsync(file.Id);
|
||||
|
||||
if (!skipData)
|
||||
await DeleteFileDataAsync(file);
|
||||
{
|
||||
var hasOtherReferences = await db.Files
|
||||
.AnyAsync(f => f.ObjectId == file.ObjectId && f.Id != file.Id);
|
||||
|
||||
if (!hasOtherReferences)
|
||||
await DeleteFileDataAsync(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteFileDataAsync(SnCloudFile file, bool force = false)
|
||||
{
|
||||
if (!file.PoolId.HasValue) return;
|
||||
if (file.ObjectId == null) return;
|
||||
|
||||
var replicas = await db.FileReplicas
|
||||
.Where(r => r.ObjectId == file.ObjectId)
|
||||
.ToListAsync();
|
||||
|
||||
if (replicas.Count == 0)
|
||||
{
|
||||
logger.LogWarning("No replicas found for file object {ObjectId}", file.ObjectId);
|
||||
return;
|
||||
}
|
||||
|
||||
var primaryReplica = replicas.FirstOrDefault(r => r.IsPrimary);
|
||||
if (primaryReplica == null)
|
||||
{
|
||||
logger.LogWarning("No primary replica found for file object {ObjectId}", file.ObjectId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (primaryReplica.PoolId == null)
|
||||
{
|
||||
logger.LogWarning("Primary replica has no pool ID for file object {ObjectId}", file.ObjectId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
{
|
||||
var sameOriginFiles = await db.Files
|
||||
.Where(f => f.StorageId == file.StorageId && f.Id != file.Id)
|
||||
.Where(f => f.ObjectId == file.ObjectId && f.Id != file.Id)
|
||||
.Select(f => f.Id)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -499,16 +575,16 @@ public class FileService(
|
||||
return;
|
||||
}
|
||||
|
||||
var dest = await GetRemoteStorageConfig(file.PoolId.Value);
|
||||
if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}");
|
||||
var dest = await GetRemoteStorageConfig(primaryReplica.PoolId.Value);
|
||||
if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {primaryReplica.PoolId}");
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to configure client for remote destination '{file.PoolId}'"
|
||||
$"Failed to configure client for remote destination '{primaryReplica.PoolId}'"
|
||||
);
|
||||
|
||||
var bucket = dest.Bucket;
|
||||
var objectId = file.StorageId ?? file.Id;
|
||||
var objectId = primaryReplica.StorageId;
|
||||
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId)
|
||||
@@ -541,36 +617,55 @@ public class FileService(
|
||||
logger.LogWarning("Failed to delete thumbnail of file {fileId}", file.Id);
|
||||
}
|
||||
}
|
||||
|
||||
db.FileReplicas.RemoveRange(replicas);
|
||||
var fileObject = await db.FileObjects.FindAsync(file.ObjectId);
|
||||
if (fileObject != null) db.FileObjects.Remove(fileObject);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteFileDataBatchAsync(List<SnCloudFile> files)
|
||||
{
|
||||
files = files.Where(f => f.PoolId.HasValue).ToList();
|
||||
files = files.Where(f => f.ObjectId != null).ToList();
|
||||
|
||||
foreach (var fileGroup in files.GroupBy(f => f.PoolId!.Value))
|
||||
var objectIds = files.Select(f => f.ObjectId).Distinct().ToList();
|
||||
var replicas = await db.FileReplicas
|
||||
.Where(r => objectIds.Contains(r.ObjectId))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var poolGroup in replicas.Where(r => r.PoolId.HasValue).GroupBy(r => r.PoolId!.Value))
|
||||
{
|
||||
var dest = await GetRemoteStorageConfig(fileGroup.Key);
|
||||
var dest = await GetRemoteStorageConfig(poolGroup.Key);
|
||||
if (dest is null)
|
||||
throw new InvalidOperationException($"No remote storage configured for pool {fileGroup.Key}");
|
||||
throw new InvalidOperationException($"No remote storage configured for pool {poolGroup.Key}");
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to configure client for remote destination '{fileGroup.Key}'"
|
||||
$"Failed to configure client for remote destination '{poolGroup.Key}'"
|
||||
);
|
||||
|
||||
List<string> objectsToDelete = [];
|
||||
|
||||
foreach (var file in fileGroup)
|
||||
foreach (var replica in poolGroup)
|
||||
{
|
||||
objectsToDelete.Add(file.StorageId ?? file.Id);
|
||||
if (file.HasCompression) objectsToDelete.Add(file.StorageId ?? file.Id + ".compressed");
|
||||
if (file.HasThumbnail) objectsToDelete.Add(file.StorageId ?? file.Id + ".thumbnail");
|
||||
var file = files.First(f => f.ObjectId == replica.ObjectId);
|
||||
objectsToDelete.Add(replica.StorageId);
|
||||
if (file.HasCompression) objectsToDelete.Add(replica.StorageId + ".compressed");
|
||||
if (file.HasThumbnail) objectsToDelete.Add(replica.StorageId + ".thumbnail");
|
||||
}
|
||||
|
||||
await client.RemoveObjectsAsync(
|
||||
new RemoveObjectsArgs().WithBucket(dest.Bucket).WithObjects(objectsToDelete)
|
||||
);
|
||||
|
||||
db.FileReplicas.RemoveRange(poolGroup);
|
||||
}
|
||||
|
||||
var fileObjects = await db.FileObjects
|
||||
.Where(fo => objectIds.Contains(fo.Id))
|
||||
.ToListAsync();
|
||||
db.FileObjects.RemoveRange(fileObjects);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<SnFileBundle?> GetBundleAsync(Guid id, Guid accountId)
|
||||
@@ -607,7 +702,7 @@ public class FileService(
|
||||
return await GetRemoteStorageConfig(id);
|
||||
}
|
||||
|
||||
public IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
|
||||
public static IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
|
||||
{
|
||||
var client = new MinioClient()
|
||||
.WithEndpoint(dest.Endpoint)
|
||||
@@ -620,72 +715,16 @@ public class FileService(
|
||||
|
||||
internal async Task _PurgeCacheAsync(string fileId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{fileId}";
|
||||
var cacheKey = string.Concat(CacheKeyPrefix, fileId);
|
||||
await cache.RemoveAsync(cacheKey);
|
||||
}
|
||||
|
||||
internal async Task _PurgeCacheRangeAsync(IEnumerable<string> fileIds)
|
||||
private async Task _PurgeCacheRangeAsync(IEnumerable<string> fileIds)
|
||||
{
|
||||
var tasks = fileIds.Select(_PurgeCacheAsync);
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task<List<SnCloudFile?>> LoadFromReference(List<SnCloudFileReferenceObject> references)
|
||||
{
|
||||
var cachedFiles = new Dictionary<string, SnCloudFile>();
|
||||
var uncachedIds = new List<string>();
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{reference.Id}";
|
||||
var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey);
|
||||
|
||||
if (cachedFile != null)
|
||||
{
|
||||
cachedFiles[reference.Id] = cachedFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
uncachedIds.Add(reference.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (uncachedIds.Count > 0)
|
||||
{
|
||||
var dbFiles = await db.Files
|
||||
.Where(f => uncachedIds.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var file in dbFiles)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{file.Id}";
|
||||
await cache.SetAsync(cacheKey, file, CacheDuration);
|
||||
cachedFiles[file.Id] = file;
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
.. references
|
||||
.Select(r => cachedFiles.GetValueOrDefault(r.Id))
|
||||
.Where(f => f != null)
|
||||
];
|
||||
}
|
||||
|
||||
public async Task<int> GetReferenceCountAsync(string fileId)
|
||||
{
|
||||
return await db.FileReferences
|
||||
.Where(r => r.FileId == fileId)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsReferencedAsync(string fileId)
|
||||
{
|
||||
return await db.FileReferences
|
||||
.Where(r => r.FileId == fileId)
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
private static bool IsIgnoredField(string fieldName)
|
||||
{
|
||||
var gpsFields = new[]
|
||||
@@ -709,8 +748,6 @@ public class FileService(
|
||||
.Where(f => f.AccountId == accountId && f.IsMarkedRecycle)
|
||||
.ToListAsync();
|
||||
var count = files.Count;
|
||||
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
|
||||
await Task.WhenAll(tasks);
|
||||
var fileIds = files.Select(f => f.Id).ToList();
|
||||
await _PurgeCacheRangeAsync(fileIds);
|
||||
db.RemoveRange(files);
|
||||
@@ -724,8 +761,6 @@ public class FileService(
|
||||
.Where(f => f.AccountId == accountId && fileIds.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
var count = files.Count;
|
||||
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
|
||||
await Task.WhenAll(tasks);
|
||||
var fileIdsList = files.Select(f => f.Id).ToList();
|
||||
await _PurgeCacheRangeAsync(fileIdsList);
|
||||
db.RemoveRange(files);
|
||||
@@ -735,12 +770,16 @@ public class FileService(
|
||||
|
||||
public async Task<int> DeletePoolRecycledFilesAsync(Guid poolId)
|
||||
{
|
||||
var fileIdsWithReplicas = await db.FileReplicas
|
||||
.Where(r => r.PoolId == poolId)
|
||||
.Select(r => r.ObjectId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var files = await db.Files
|
||||
.Where(f => f.PoolId == poolId && f.IsMarkedRecycle)
|
||||
.Where(f => fileIdsWithReplicas.Contains(f.Id) && f.IsMarkedRecycle)
|
||||
.ToListAsync();
|
||||
var count = files.Count;
|
||||
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
|
||||
await Task.WhenAll(tasks);
|
||||
var fileIds = files.Select(f => f.Id).ToList();
|
||||
await _PurgeCacheRangeAsync(fileIds);
|
||||
db.RemoveRange(files);
|
||||
@@ -754,8 +793,6 @@ public class FileService(
|
||||
.Where(f => f.IsMarkedRecycle)
|
||||
.ToListAsync();
|
||||
var count = files.Count;
|
||||
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
|
||||
await Task.WhenAll(tasks);
|
||||
var fileIds = files.Select(f => f.Id).ToList();
|
||||
await _PurgeCacheRangeAsync(fileIds);
|
||||
db.RemoveRange(files);
|
||||
@@ -763,25 +800,43 @@ public class FileService(
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<string> CreateFastUploadLinkAsync(SnCloudFile file)
|
||||
public async Task SetPublicAsync(string fileId)
|
||||
{
|
||||
if (file.PoolId is null) throw new InvalidOperationException("Pool ID is null");
|
||||
var existingPermission = await db.FilePermissions
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.FileId == fileId &&
|
||||
p.SubjectType == SnFilePermissionType.Anyone &&
|
||||
p.Permission == SnFilePermissionLevel.Read);
|
||||
|
||||
var dest = await GetRemoteStorageConfig(file.PoolId.Value);
|
||||
if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}");
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to configure client for remote destination '{file.PoolId}'"
|
||||
);
|
||||
if (existingPermission != null)
|
||||
return;
|
||||
|
||||
var url = await client.PresignedPutObjectAsync(
|
||||
new PresignedPutObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(file.Id)
|
||||
.WithExpiry(60 * 60 * 24)
|
||||
);
|
||||
return url;
|
||||
var permission = new SnFilePermission
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FileId = fileId,
|
||||
SubjectType = SnFilePermissionType.Anyone,
|
||||
SubjectId = string.Empty,
|
||||
Permission = SnFilePermissionLevel.Read
|
||||
};
|
||||
|
||||
db.FilePermissions.Add(permission);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UnsetPublicAsync(string fileId)
|
||||
{
|
||||
var permission = await db.FilePermissions
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.FileId == fileId &&
|
||||
p.SubjectType == SnFilePermissionType.Anyone &&
|
||||
p.Permission == SnFilePermissionLevel.Read);
|
||||
|
||||
if (permission == null)
|
||||
return;
|
||||
|
||||
db.FilePermissions.Remove(permission);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,13 +848,12 @@ file class UpdatableCloudFile(SnCloudFile file)
|
||||
public Dictionary<string, object?>? UserMeta { get; set; } = file.UserMeta;
|
||||
public bool IsMarkedRecycle { get; set; } = file.IsMarkedRecycle;
|
||||
|
||||
public Expression<Func<SetPropertyCalls<SnCloudFile>, SetPropertyCalls<SnCloudFile>>> ToSetPropertyCalls()
|
||||
public Action<UpdateSettersBuilder<SnCloudFile>> ToSetPropertyCalls()
|
||||
{
|
||||
var userMeta = UserMeta ?? [];
|
||||
return setter => setter
|
||||
.SetProperty(f => f.Name, Name)
|
||||
.SetProperty(f => f.Description, Description)
|
||||
.SetProperty(f => f.FileMeta, FileMeta)
|
||||
.SetProperty(f => f.UserMeta, userMeta)
|
||||
.SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
@@ -7,7 +6,7 @@ namespace DysonNetwork.Drive.Storage
|
||||
{
|
||||
public class FileServiceGrpc(FileService fileService) : Shared.Proto.FileService.FileServiceBase
|
||||
{
|
||||
public override async Task<Shared.Proto.CloudFile> GetFile(GetFileRequest request, ServerCallContext context)
|
||||
public override async Task<CloudFile> GetFile(GetFileRequest request, ServerCallContext context)
|
||||
{
|
||||
var file = await fileService.GetFileAsync(request.Id);
|
||||
return file?.ToProtoValue() ?? throw new RpcException(new Status(StatusCode.NotFound, "File not found"));
|
||||
@@ -19,7 +18,7 @@ namespace DysonNetwork.Drive.Storage
|
||||
return new GetFileBatchResponse { Files = { files.Select(f => f.ToProtoValue()) } };
|
||||
}
|
||||
|
||||
public override async Task<Shared.Proto.CloudFile> UpdateFile(UpdateFileRequest request,
|
||||
public override async Task<CloudFile> UpdateFile(UpdateFileRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var file = await fileService.GetFileAsync(request.File.Id);
|
||||
@@ -41,31 +40,22 @@ namespace DysonNetwork.Drive.Storage
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<LoadFromReferenceResponse> LoadFromReference(
|
||||
LoadFromReferenceRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
// Assuming CloudFileReferenceObject is a simple class/struct that holds an ID
|
||||
// You might need to define this or adjust the LoadFromReference method in FileService
|
||||
var references = request.ReferenceIds.Select(id => new SnCloudFileReferenceObject { Id = id }).ToList();
|
||||
var files = await fileService.LoadFromReference(references);
|
||||
var response = new LoadFromReferenceResponse();
|
||||
response.Files.AddRange(files.Where(f => f != null).Select(f => f!.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<IsReferencedResponse> IsReferenced(IsReferencedRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var isReferenced = await fileService.IsReferencedAsync(request.FileId);
|
||||
return new IsReferencedResponse { IsReferenced = isReferenced };
|
||||
}
|
||||
|
||||
public override async Task<Empty> PurgeCache(PurgeCacheRequest request, ServerCallContext context)
|
||||
{
|
||||
await fileService._PurgeCacheAsync(request.FileId);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<Empty> SetFilePublic(SetFilePublicRequest request, ServerCallContext context)
|
||||
{
|
||||
await fileService.SetPublicAsync(request.FileId);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<Empty> UnsetFilePublic(UnsetFilePublicRequest request, ServerCallContext context)
|
||||
{
|
||||
await fileService.UnsetPublicAsync(request.FileId);
|
||||
return new Empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ using DysonNetwork.Drive.Billing;
|
||||
using DysonNetwork.Drive.Index;
|
||||
using DysonNetwork.Drive.Storage.Model;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -64,7 +64,10 @@ public class FileUploadController(
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
// Check if a file with the same hash already exists
|
||||
var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Hash == request.Hash);
|
||||
var existingFile = await db.Files
|
||||
.Include(f => f.Object)
|
||||
.Where(f => f.Object != null && f.Object.Hash == request.Hash)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingFile != null)
|
||||
{
|
||||
// Create the file index if a path is provided, even for existing files
|
||||
|
||||
@@ -8,8 +8,8 @@ public static class FileUploadedEvent
|
||||
public record FileUploadedEventPayload(
|
||||
string FileId,
|
||||
Guid RemoteId,
|
||||
string StorageId,
|
||||
string ContentType,
|
||||
string? StorageId,
|
||||
string? ContentType,
|
||||
string ProcessingFilePath,
|
||||
bool IsTempFile
|
||||
);
|
||||
|
||||
@@ -161,7 +161,6 @@ public class PersistentTask : ModelBase
|
||||
public long? EstimatedDurationSeconds { get; set; }
|
||||
}
|
||||
|
||||
// Backward compatibility - UploadTask inherits from PersistentTask
|
||||
public class PersistentUploadTask : PersistentTask
|
||||
{
|
||||
public PersistentUploadTask()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace DysonNetwork.Drive.Storage.Options;
|
||||
|
||||
public class FileReanalysisOptions
|
||||
{
|
||||
public bool Enabled { get; init; } = true;
|
||||
public bool ValidateCompression { get; init; } = true;
|
||||
public bool ValidateThumbnails { get; init; } = true;
|
||||
}
|
||||
@@ -664,16 +664,54 @@ public class PersistentTaskService(
|
||||
if (cachedTask is not null)
|
||||
return cachedTask;
|
||||
|
||||
var task = await db.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.FirstOrDefaultAsync(t => t.TaskId == taskId && t.Status == TaskStatus.InProgress);
|
||||
var baseTask = await db.Tasks
|
||||
.FirstOrDefaultAsync(t => t.TaskId == taskId && t.Type == TaskType.FileUpload && t.Status == TaskStatus.InProgress);
|
||||
|
||||
if (task is not null)
|
||||
await SetCacheAsync(task);
|
||||
if (baseTask is null)
|
||||
return null;
|
||||
|
||||
var task = ConvertToUploadTask(baseTask);
|
||||
await SetCacheAsync(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a base PersistentTask to PersistentUploadTask
|
||||
/// </summary>
|
||||
private PersistentUploadTask ConvertToUploadTask(PersistentTask baseTask)
|
||||
{
|
||||
return new PersistentUploadTask
|
||||
{
|
||||
Id = baseTask.Id,
|
||||
TaskId = baseTask.TaskId,
|
||||
Name = baseTask.Name,
|
||||
Description = baseTask.Description,
|
||||
Type = baseTask.Type,
|
||||
Status = baseTask.Status,
|
||||
AccountId = baseTask.AccountId,
|
||||
Progress = baseTask.Progress,
|
||||
Parameters = baseTask.Parameters,
|
||||
Results = baseTask.Results,
|
||||
ErrorMessage = baseTask.ErrorMessage,
|
||||
StartedAt = baseTask.StartedAt,
|
||||
CompletedAt = baseTask.CompletedAt,
|
||||
ExpiredAt = baseTask.ExpiredAt,
|
||||
LastActivity = baseTask.LastActivity,
|
||||
Priority = baseTask.Priority,
|
||||
EstimatedDurationSeconds = baseTask.EstimatedDurationSeconds,
|
||||
CreatedAt = baseTask.CreatedAt,
|
||||
UpdatedAt = baseTask.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of base PersistentTasks to PersistentUploadTasks
|
||||
/// </summary>
|
||||
private List<PersistentUploadTask> ConvertToUploadTasks(List<PersistentTask> baseTasks)
|
||||
{
|
||||
return baseTasks.Select(ConvertToUploadTask).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates chunk upload progress
|
||||
/// </summary>
|
||||
@@ -697,8 +735,7 @@ public class PersistentTaskService(
|
||||
|
||||
// Use ExecuteUpdateAsync to update the Parameters dictionary directly
|
||||
var updatedRows = await db.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.Where(t => t.TaskId == taskId)
|
||||
.Where(t => t.TaskId == taskId && t.Type == TaskType.FileUpload)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(t => t.Parameters, ParameterHelper.Untyped(parameters))
|
||||
.SetProperty(t => t.LastActivity, now)
|
||||
@@ -754,7 +791,7 @@ public class PersistentTaskService(
|
||||
int limit = 50
|
||||
)
|
||||
{
|
||||
var query = db.Tasks.OfType<PersistentUploadTask>().Where(t => t.AccountId == accountId);
|
||||
var query = db.Tasks.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId);
|
||||
|
||||
// Apply status filter
|
||||
if (status.HasValue)
|
||||
@@ -766,19 +803,9 @@ public class PersistentTaskService(
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
// Apply sorting
|
||||
IOrderedQueryable<PersistentUploadTask> orderedQuery;
|
||||
IOrderedQueryable<PersistentTask> orderedQuery;
|
||||
switch (sortBy?.ToLower())
|
||||
{
|
||||
case "filename":
|
||||
orderedQuery = sortDescending
|
||||
? query.OrderByDescending(t => t.FileName)
|
||||
: query.OrderBy(t => t.FileName);
|
||||
break;
|
||||
case "filesize":
|
||||
orderedQuery = sortDescending
|
||||
? query.OrderByDescending(t => t.FileSize)
|
||||
: query.OrderBy(t => t.FileSize);
|
||||
break;
|
||||
case "created":
|
||||
orderedQuery = sortDescending
|
||||
? query.OrderByDescending(t => t.CreatedAt)
|
||||
@@ -798,11 +825,27 @@ public class PersistentTaskService(
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
var items = await orderedQuery
|
||||
var baseTasks = await orderedQuery
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
var items = ConvertToUploadTasks(baseTasks);
|
||||
|
||||
// Sort by derived properties if needed (filename, filesize)
|
||||
if (sortBy?.ToLower() == "filename")
|
||||
{
|
||||
items = sortDescending
|
||||
? items.OrderByDescending(t => t.FileName).ToList()
|
||||
: items.OrderBy(t => t.FileName).ToList();
|
||||
}
|
||||
else if (sortBy?.ToLower() == "filesize")
|
||||
{
|
||||
items = sortDescending
|
||||
? items.OrderByDescending(t => t.FileSize).ToList()
|
||||
: items.OrderBy(t => t.FileSize).ToList();
|
||||
}
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
@@ -811,11 +854,12 @@ public class PersistentTaskService(
|
||||
/// </summary>
|
||||
public async Task<UserUploadStats> GetUserUploadStatsAsync(Guid accountId)
|
||||
{
|
||||
var tasks = await db.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.Where(t => t.AccountId == accountId)
|
||||
var baseTasks = await db.Tasks
|
||||
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId)
|
||||
.ToListAsync();
|
||||
|
||||
var tasks = ConvertToUploadTasks(baseTasks);
|
||||
|
||||
var stats = new UserUploadStats
|
||||
{
|
||||
TotalTasks = tasks.Count,
|
||||
@@ -850,8 +894,7 @@ public class PersistentTaskService(
|
||||
public async Task<int> CleanupUserFailedTasksAsync(Guid accountId)
|
||||
{
|
||||
var failedTasks = await db.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.Where(t => t.AccountId == accountId &&
|
||||
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId &&
|
||||
(t.Status == TaskStatus.Failed || t.Status == TaskStatus.Expired))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -883,12 +926,13 @@ public class PersistentTaskService(
|
||||
/// </summary>
|
||||
public async Task<List<PersistentUploadTask>> GetRecentUserTasksAsync(Guid accountId, int limit = 10)
|
||||
{
|
||||
return await db.Tasks
|
||||
.OfType<PersistentUploadTask>()
|
||||
.Where(t => t.AccountId == accountId)
|
||||
var baseTasks = await db.Tasks
|
||||
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId)
|
||||
.OrderByDescending(t => t.LastActivity)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
return ConvertToUploadTasks(baseTasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -74,11 +74,6 @@
|
||||
"FromName": "Alphabot",
|
||||
"SubjectPrefix": "Solar Network"
|
||||
},
|
||||
"RealtimeChat": {
|
||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
||||
"ApiKey": "APIs6TiL8wj3A4j",
|
||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
||||
},
|
||||
"GeoIp": {
|
||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||
},
|
||||
@@ -112,10 +107,19 @@
|
||||
}
|
||||
},
|
||||
"Cache": {
|
||||
"Serializer": "MessagePack"
|
||||
"Serializer": "JSON"
|
||||
},
|
||||
"AccessToken": {
|
||||
"Secret": "dyson-network-default-access-token-secret-change-in-production"
|
||||
},
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
]
|
||||
],
|
||||
"FileReanalysis": {
|
||||
"Enabled": true,
|
||||
"ValidateCompression": true,
|
||||
"ValidateThumbnails": true,
|
||||
"DelayMs": 10000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="10.1.0" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.9.50">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
25
DysonNetwork.Gateway/Health/GatewayConstant.cs
Normal file
25
DysonNetwork.Gateway/Health/GatewayConstant.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace DysonNetwork.Gateway.Health;
|
||||
|
||||
public abstract class GatewayConstant
|
||||
{
|
||||
public static readonly string[] ServiceNames =
|
||||
[
|
||||
"ring",
|
||||
"pass",
|
||||
"drive",
|
||||
"sphere",
|
||||
"develop",
|
||||
"insight",
|
||||
"zone",
|
||||
"messager"
|
||||
];
|
||||
|
||||
// Core services stands with w/o these services the functional of entire app will broke.
|
||||
public static readonly string[] CoreServiceNames =
|
||||
[
|
||||
"ring",
|
||||
"pass",
|
||||
"drive",
|
||||
"sphere"
|
||||
];
|
||||
}
|
||||
60
DysonNetwork.Gateway/Health/GatewayHealthAggregator.cs
Normal file
60
DysonNetwork.Gateway/Health/GatewayHealthAggregator.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Gateway.Health;
|
||||
|
||||
public class GatewayHealthAggregator(IHttpClientFactory httpClientFactory, GatewayReadinessStore store)
|
||||
: BackgroundService
|
||||
{
|
||||
private async Task<ServiceHealthState> CheckService(string serviceName)
|
||||
{
|
||||
var client = httpClientFactory.CreateClient("health");
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
try
|
||||
{
|
||||
// Use the service discovery to lookup service
|
||||
// The service defaults give every single service a health endpoint that we can use here
|
||||
using var response = await client.GetAsync($"http://{serviceName}/health");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return new ServiceHealthState(
|
||||
serviceName,
|
||||
true,
|
||||
now,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return new ServiceHealthState(
|
||||
serviceName,
|
||||
false,
|
||||
now,
|
||||
$"StatusCode: {(int)response.StatusCode}"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ServiceHealthState(
|
||||
serviceName,
|
||||
false,
|
||||
now,
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
foreach (var service in GatewayConstant.ServiceNames)
|
||||
{
|
||||
var result = await CheckService(service);
|
||||
store.Update(result);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
DysonNetwork.Gateway/Health/GatewayReadinessMiddleware.cs
Normal file
35
DysonNetwork.Gateway/Health/GatewayReadinessMiddleware.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace DysonNetwork.Gateway.Health;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
public sealed class GatewayReadinessMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, GatewayReadinessStore store)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/health"))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var readiness = store.Current;
|
||||
|
||||
// Only core services participate in readiness gating
|
||||
var notReadyCoreServices = readiness.Services
|
||||
.Where(kv => GatewayConstant.CoreServiceNames.Contains(kv.Key))
|
||||
.Where(kv => !kv.Value.IsHealthy)
|
||||
.Select(kv => kv.Key)
|
||||
.ToArray();
|
||||
|
||||
if (notReadyCoreServices.Length > 0)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
var unavailableServices = string.Join(", ", notReadyCoreServices);
|
||||
context.Response.Headers["X-NotReady"] = unavailableServices;
|
||||
await context.Response.WriteAsync("Solar Network is warming up. Try again later please.");
|
||||
return;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
76
DysonNetwork.Gateway/Health/GatewayReadinessStore.cs
Normal file
76
DysonNetwork.Gateway/Health/GatewayReadinessStore.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Gateway.Health;
|
||||
|
||||
public record ServiceHealthState(
|
||||
string ServiceName,
|
||||
bool IsHealthy,
|
||||
Instant LastChecked,
|
||||
string? Error
|
||||
);
|
||||
|
||||
public record GatewayReadinessState(
|
||||
bool IsReady,
|
||||
IReadOnlyDictionary<string, ServiceHealthState> Services,
|
||||
Instant LastUpdated
|
||||
);
|
||||
|
||||
public class GatewayReadinessStore
|
||||
{
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
private readonly Dictionary<string, ServiceHealthState> _services = new();
|
||||
|
||||
public GatewayReadinessState Current { get; private set; } = new(
|
||||
IsReady: false,
|
||||
Services: new Dictionary<string, ServiceHealthState>(),
|
||||
LastUpdated: SystemClock.Instance.GetCurrentInstant()
|
||||
);
|
||||
|
||||
public IReadOnlyCollection<string> ServiceNames => _services.Keys;
|
||||
|
||||
public GatewayReadinessStore()
|
||||
{
|
||||
InitializeServices(GatewayConstant.ServiceNames);
|
||||
}
|
||||
|
||||
private void InitializeServices(IEnumerable<string> serviceNames)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_services.Clear();
|
||||
|
||||
foreach (var name in serviceNames)
|
||||
{
|
||||
_services[name] = new ServiceHealthState(
|
||||
name,
|
||||
IsHealthy: false,
|
||||
LastChecked: SystemClock.Instance.GetCurrentInstant(),
|
||||
Error: "Not checked yet"
|
||||
);
|
||||
}
|
||||
|
||||
RecalculateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(ServiceHealthState state)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_services[state.ServiceName] = state;
|
||||
RecalculateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecalculateLocked()
|
||||
{
|
||||
var isReady = _services.Count > 0 && _services.Values.All(s => s.IsHealthy);
|
||||
|
||||
Current = new GatewayReadinessState(
|
||||
IsReady: isReady,
|
||||
Services: new Dictionary<string, ServiceHealthState>(_services),
|
||||
LastUpdated: SystemClock.Instance.GetCurrentInstant()
|
||||
);
|
||||
}
|
||||
}
|
||||
14
DysonNetwork.Gateway/Health/GatewayStatusController.cs
Normal file
14
DysonNetwork.Gateway/Health/GatewayStatusController.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Gateway.Health;
|
||||
|
||||
[ApiController]
|
||||
[Route("/health")]
|
||||
public class GatewayStatusController(GatewayReadinessStore readinessStore) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult<GatewayReadinessState> GetHealthStatus()
|
||||
{
|
||||
return Ok(readinessStore.Current);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Gateway.Health;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using Yarp.ReverseProxy.Configuration;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -9,17 +14,19 @@ builder.AddServiceDefaults();
|
||||
|
||||
builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue, enableGrpc: false);
|
||||
|
||||
builder.Services.AddSingleton<GatewayReadinessStore>();
|
||||
builder.Services.AddHostedService<GatewayHealthAggregator>();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(
|
||||
policy =>
|
||||
{
|
||||
policy.SetIsOriginAllowed(origin => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
.WithExposedHeaders("X-Total");
|
||||
});
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.SetIsOriginAllowed(origin => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
.WithExposedHeaders("X-Total", "X-NotReady");
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
@@ -40,23 +47,22 @@ builder.Services.AddRateLimiter(options =>
|
||||
});
|
||||
|
||||
options.OnRejected = async (context, token) =>
|
||||
{
|
||||
// Log the rejected IP
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("RateLimiter");
|
||||
{
|
||||
// Log the rejected IP
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("RateLimiter");
|
||||
|
||||
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
logger.LogWarning("Rate limit exceeded for IP: {IP}", ip);
|
||||
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
logger.LogWarning("Rate limit exceeded for IP: {IP}", ip);
|
||||
|
||||
// Respond to the client
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
"Rate limit exceeded. Try again later.", token);
|
||||
};
|
||||
// Respond to the client
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
await context.HttpContext.Response.WriteAsync(
|
||||
"Rate limit exceeded. Try again later.", token);
|
||||
};
|
||||
});
|
||||
|
||||
var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop", "insight", "zone" };
|
||||
|
||||
var specialRoutes = new[]
|
||||
{
|
||||
@@ -80,13 +86,19 @@ var specialRoutes = new[]
|
||||
},
|
||||
new RouteConfig
|
||||
{
|
||||
RouteId = "drive-tus",
|
||||
ClusterId = "drive",
|
||||
Match = new RouteMatch { Path = "/api/tus" }
|
||||
}
|
||||
RouteId = "sphere-webfinger",
|
||||
ClusterId = "sphere",
|
||||
Match = new RouteMatch { Path = "/.well-known/webfinger" }
|
||||
},
|
||||
new RouteConfig
|
||||
{
|
||||
RouteId = "sphere-activitypub",
|
||||
ClusterId = "sphere",
|
||||
Match = new RouteMatch { Path = "/activitypub/{**catch-all}" }
|
||||
},
|
||||
};
|
||||
|
||||
var apiRoutes = serviceNames.Select(serviceName =>
|
||||
var apiRoutes = GatewayConstant.ServiceNames.Select(serviceName =>
|
||||
{
|
||||
var apiPath = serviceName switch
|
||||
{
|
||||
@@ -105,7 +117,7 @@ var apiRoutes = serviceNames.Select(serviceName =>
|
||||
};
|
||||
});
|
||||
|
||||
var swaggerRoutes = serviceNames.Select(serviceName => new RouteConfig
|
||||
var swaggerRoutes = GatewayConstant.ServiceNames.Select(serviceName => new RouteConfig
|
||||
{
|
||||
RouteId = $"{serviceName}-swagger",
|
||||
ClusterId = serviceName,
|
||||
@@ -119,7 +131,7 @@ var swaggerRoutes = serviceNames.Select(serviceName => new RouteConfig
|
||||
|
||||
var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray();
|
||||
|
||||
var clusters = serviceNames.Select(serviceName => new ClusterConfig
|
||||
var clusters = GatewayConstant.ServiceNames.Select(serviceName => new ClusterConfig
|
||||
{
|
||||
ClusterId = serviceName,
|
||||
HealthCheck = new HealthCheckConfig
|
||||
@@ -131,7 +143,7 @@ var clusters = serviceNames.Select(serviceName => new ClusterConfig
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Path = "/health"
|
||||
},
|
||||
Passive = new()
|
||||
Passive = new PassiveHealthCheckConfig
|
||||
{
|
||||
Enabled = true
|
||||
}
|
||||
@@ -147,7 +159,14 @@ builder.Services
|
||||
.LoadFromMemory(routes, clusters)
|
||||
.AddServiceDiscoveryDestinationResolver();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -155,12 +174,14 @@ var forwardedHeadersOptions = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.All
|
||||
};
|
||||
forwardedHeadersOptions.KnownNetworks.Clear();
|
||||
forwardedHeadersOptions.KnownIPNetworks.Clear();
|
||||
forwardedHeadersOptions.KnownProxies.Clear();
|
||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseMiddleware<GatewayReadinessMiddleware>();
|
||||
|
||||
app.MapReverseProxy().RequireRateLimiting("fixed");
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"Cache": {
|
||||
"Serializer": "MessagePack"
|
||||
"Serializer": "JSON"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"SiteUrl": "http://localhost:3000",
|
||||
|
||||
@@ -15,6 +15,10 @@ public class AppDatabase(
|
||||
public DbSet<SnThinkingThought> ThinkingThoughts { get; set; }
|
||||
public DbSet<SnUnpaidAccount> UnpaidAccounts { get; set; }
|
||||
|
||||
public DbSet<SnWebArticle> WebArticles { get; set; }
|
||||
public DbSet<SnWebFeed> WebFeeds { get; set; }
|
||||
public DbSet<SnWebFeedSubscription> WebFeedSubscriptions { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
@@ -38,6 +42,8 @@ public class AppDatabase(
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Ignore<SnAccount>();
|
||||
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libkrb5-3 \
|
||||
libgssapi-krb5-2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
USER app
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -7,17 +7,27 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.2" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.76.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.76.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.67.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.68.0" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="System.ServiceModel.Syndication" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -28,4 +38,8 @@
|
||||
<Folder Include="Controllers\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Remove="..\DysonNetwork.Shared\Proto\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
358
DysonNetwork.Insight/Migrations/20260102075604_AddWebFeed.Designer.cs
generated
Normal file
358
DysonNetwork.Insight/Migrations/20260102075604_AddWebFeed.Designer.cs
generated
Normal file
@@ -0,0 +1,358 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Insight;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Insight.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20260102075604_AddWebFeed")]
|
||||
partial class AddWebFeed
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsPublic")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_public");
|
||||
|
||||
b.Property<long>("PaidToken")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("paid_token");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("topic");
|
||||
|
||||
b.Property<long>("TotalToken")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("total_token");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_thinking_sequences");
|
||||
|
||||
b.ToTable("thinking_sequences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<List<SnCloudFileReferenceObject>>("Files")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("files");
|
||||
|
||||
b.Property<string>("ModelName")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("model_name");
|
||||
|
||||
b.Property<List<SnThinkingMessagePart>>("Parts")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parts");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<Guid>("SequenceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("sequence_id");
|
||||
|
||||
b.Property<long>("TokenCount")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("token_count");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_thinking_thoughts");
|
||||
|
||||
b.HasIndex("SequenceId")
|
||||
.HasDatabaseName("ix_thinking_thoughts_sequence_id");
|
||||
|
||||
b.ToTable("thinking_thoughts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnUnpaidAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("AccountId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<DateTime>("MarkedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("marked_at");
|
||||
|
||||
b.HasKey("AccountId")
|
||||
.HasName("pk_unpaid_accounts");
|
||||
|
||||
b.ToTable("unpaid_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebArticle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("author");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("content");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid>("FeedId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("feed_id");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<LinkEmbed>("Preview")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("preview");
|
||||
|
||||
b.Property<DateTime?>("PublishedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("published_at");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_articles");
|
||||
|
||||
b.HasIndex("FeedId")
|
||||
.HasDatabaseName("ix_web_articles_feed_id");
|
||||
|
||||
b.ToTable("web_articles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeed", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<WebFeedConfig>("Config")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<LinkEmbed>("Preview")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("preview");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<string>("VerificationKey")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("verification_key");
|
||||
|
||||
b.Property<Instant?>("VerifiedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("verified_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_feeds");
|
||||
|
||||
b.ToTable("web_feeds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeedSubscription", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid>("FeedId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("feed_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_feed_subscriptions");
|
||||
|
||||
b.HasIndex("FeedId")
|
||||
.HasDatabaseName("ix_web_feed_subscriptions_feed_id");
|
||||
|
||||
b.ToTable("web_feed_subscriptions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
|
||||
.WithMany()
|
||||
.HasForeignKey("SequenceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id");
|
||||
|
||||
b.Navigation("Sequence");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebArticle", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnWebFeed", "Feed")
|
||||
.WithMany("Articles")
|
||||
.HasForeignKey("FeedId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_web_articles_web_feeds_feed_id");
|
||||
|
||||
b.Navigation("Feed");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeedSubscription", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnWebFeed", "Feed")
|
||||
.WithMany()
|
||||
.HasForeignKey("FeedId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_web_feed_subscriptions_web_feeds_feed_id");
|
||||
|
||||
b.Navigation("Feed");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeed", b =>
|
||||
{
|
||||
b.Navigation("Articles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
114
DysonNetwork.Insight/Migrations/20260102075604_AddWebFeed.cs
Normal file
114
DysonNetwork.Insight/Migrations/20260102075604_AddWebFeed.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Insight.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWebFeed : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "web_feeds",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
url = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||
title = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true),
|
||||
verified_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
verification_key = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true),
|
||||
preview = table.Column<LinkEmbed>(type: "jsonb", nullable: true),
|
||||
config = table.Column<WebFeedConfig>(type: "jsonb", nullable: false),
|
||||
publisher_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_web_feeds", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "web_articles",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
title = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
url = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||
author = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
|
||||
preview = table.Column<LinkEmbed>(type: "jsonb", nullable: true),
|
||||
content = table.Column<string>(type: "text", nullable: true),
|
||||
published_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
feed_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_web_articles", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_web_articles_web_feeds_feed_id",
|
||||
column: x => x.feed_id,
|
||||
principalTable: "web_feeds",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "web_feed_subscriptions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
feed_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_web_feed_subscriptions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_web_feed_subscriptions_web_feeds_feed_id",
|
||||
column: x => x.feed_id,
|
||||
principalTable: "web_feeds",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_web_articles_feed_id",
|
||||
table: "web_articles",
|
||||
column: "feed_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_web_feed_subscriptions_feed_id",
|
||||
table: "web_feed_subscriptions",
|
||||
column: "feed_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "web_articles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "web_feed_subscriptions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "web_feeds");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Insight;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -20,7 +21,7 @@ namespace DysonNetwork.Insight.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.11")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -143,6 +144,171 @@ namespace DysonNetwork.Insight.Migrations
|
||||
b.ToTable("unpaid_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebArticle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("author");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("content");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid>("FeedId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("feed_id");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<LinkEmbed>("Preview")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("preview");
|
||||
|
||||
b.Property<DateTime?>("PublishedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("published_at");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_articles");
|
||||
|
||||
b.HasIndex("FeedId")
|
||||
.HasDatabaseName("ix_web_articles_feed_id");
|
||||
|
||||
b.ToTable("web_articles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeed", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<WebFeedConfig>("Config")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<LinkEmbed>("Preview")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("preview");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<string>("VerificationKey")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("verification_key");
|
||||
|
||||
b.Property<Instant?>("VerifiedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("verified_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_feeds");
|
||||
|
||||
b.ToTable("web_feeds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeedSubscription", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Guid>("FeedId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("feed_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_web_feed_subscriptions");
|
||||
|
||||
b.HasIndex("FeedId")
|
||||
.HasDatabaseName("ix_web_feed_subscriptions_feed_id");
|
||||
|
||||
b.ToTable("web_feed_subscriptions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
|
||||
@@ -154,6 +320,35 @@ namespace DysonNetwork.Insight.Migrations
|
||||
|
||||
b.Navigation("Sequence");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebArticle", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnWebFeed", "Feed")
|
||||
.WithMany("Articles")
|
||||
.HasForeignKey("FeedId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_web_articles_web_feeds_feed_id");
|
||||
|
||||
b.Navigation("Feed");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeedSubscription", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnWebFeed", "Feed")
|
||||
.WithMany()
|
||||
.HasForeignKey("FeedId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_web_feed_subscriptions_web_feeds_feed_id");
|
||||
|
||||
b.Navigation("Feed");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWebFeed", b =>
|
||||
{
|
||||
b.Navigation("Articles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using DysonNetwork.Insight;
|
||||
using DysonNetwork.Insight.Startup;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
33
DysonNetwork.Insight/Reader/ScrapedArticle.cs
Normal file
33
DysonNetwork.Insight/Reader/ScrapedArticle.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using EmbedLinkEmbed = DysonNetwork.Shared.Models.Embed.LinkEmbed;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
public class ScrapedArticle
|
||||
{
|
||||
public EmbedLinkEmbed LinkEmbed { get; set; } = null!;
|
||||
public string? Content { get; set; }
|
||||
|
||||
public Shared.Proto.ScrapedArticle ToProtoValue()
|
||||
{
|
||||
var proto = new Shared.Proto.ScrapedArticle
|
||||
{
|
||||
LinkEmbed = LinkEmbed.ToProtoValue()
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(Content))
|
||||
proto.Content = Content;
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
public static ScrapedArticle FromProtoValue(Shared.Proto.ScrapedArticle proto)
|
||||
{
|
||||
return new ScrapedArticle
|
||||
{
|
||||
LinkEmbed = EmbedLinkEmbed.FromProtoValue(proto.LinkEmbed),
|
||||
Content = proto.Content == "" ? null : proto.Content
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/feeds/articles")]
|
||||
90
DysonNetwork.Insight/Reader/WebArticleGrpcService.cs
Normal file
90
DysonNetwork.Insight/Reader/WebArticleGrpcService.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
public class WebArticleGrpcService(AppDatabase db) : WebArticleService.WebArticleServiceBase
|
||||
{
|
||||
public override async Task<GetWebArticleResponse> GetWebArticle(
|
||||
GetWebArticleRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (!Guid.TryParse(request.Id, out var id))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid id"));
|
||||
|
||||
var article = await db.WebArticles
|
||||
.Include(a => a.Feed)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
return article == null
|
||||
? throw new RpcException(new Status(StatusCode.NotFound, "article not found"))
|
||||
: new GetWebArticleResponse { Article = article.ToProtoValue() };
|
||||
}
|
||||
|
||||
public override async Task<GetWebArticleBatchResponse> GetWebArticleBatch(
|
||||
GetWebArticleBatchRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
var ids = request.Ids
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out _))
|
||||
.Select(Guid.Parse)
|
||||
.ToList();
|
||||
|
||||
if (ids.Count == 0)
|
||||
return new GetWebArticleBatchResponse();
|
||||
|
||||
var articles = await db.WebArticles
|
||||
.Include(a => a.Feed)
|
||||
.Where(a => ids.Contains(a.Id))
|
||||
.ToListAsync();
|
||||
|
||||
var response = new GetWebArticleBatchResponse();
|
||||
response.Articles.AddRange(articles.Select(a => a.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<ListWebArticlesResponse> ListWebArticles(
|
||||
ListWebArticlesRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (!Guid.TryParse(request.FeedId, out var feedId))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid feed_id"));
|
||||
|
||||
var query = db.WebArticles
|
||||
.Include(a => a.Feed)
|
||||
.Where(a => a.FeedId == feedId);
|
||||
|
||||
var articles = await query.ToListAsync();
|
||||
|
||||
var response = new ListWebArticlesResponse
|
||||
{
|
||||
TotalSize = articles.Count
|
||||
};
|
||||
response.Articles.AddRange(articles.Select(a => a.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetRecentArticlesResponse> GetRecentArticles(
|
||||
GetRecentArticlesRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
var limit = request.Limit > 0 ? request.Limit : 20;
|
||||
|
||||
var articles = await db.WebArticles
|
||||
.Include(a => a.Feed)
|
||||
.OrderByDescending(a => a.PublishedAt ?? DateTime.MinValue)
|
||||
.ThenByDescending(a => a.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
var response = new GetRecentArticlesResponse();
|
||||
response.Articles.AddRange(articles.Select(a => a.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using WebFeedConfig = DysonNetwork.Shared.Models.WebFeedConfig;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("/api/publishers/{pubName}/feeds")]
|
||||
public class WebFeedController(WebFeedService webFeed, Publisher.PublisherService ps) : ControllerBase
|
||||
public class WebFeedController(WebFeedService webFeed, RemotePublisherService ps) : ControllerBase
|
||||
{
|
||||
public record WebFeedRequest(
|
||||
[MaxLength(8192)] string? Url,
|
||||
@@ -125,4 +128,63 @@ public class WebFeedController(WebFeedService webFeed, Publisher.PublisherServic
|
||||
await webFeed.ScrapeFeedAsync(feed);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/verify/init")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeedVerificationInitResult>> InitVerification([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Shared.Models.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to verify a web feed");
|
||||
|
||||
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
|
||||
if (feed == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await webFeed.GenerateVerificationCodeAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/verify")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeedVerificationResult>> VerifyOwnership([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await ps.GetPublisherByName(pubName);
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, accountId, Shared.Models.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the publisher to verify a web feed");
|
||||
|
||||
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
|
||||
if (feed == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await webFeed.VerifyOwnershipAsync(id);
|
||||
if (!result.Success)
|
||||
return BadRequest(result.Message);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
DysonNetwork.Insight/Reader/WebFeedGrpcService.cs
Normal file
55
DysonNetwork.Insight/Reader/WebFeedGrpcService.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
public class WebFeedGrpcService(WebFeedService service, AppDatabase db)
|
||||
: Shared.Proto.WebFeedService.WebFeedServiceBase
|
||||
{
|
||||
public override async Task<GetWebFeedResponse> GetWebFeed(
|
||||
GetWebFeedRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
SnWebFeed? feed = null;
|
||||
|
||||
switch (request.IdentifierCase)
|
||||
{
|
||||
case GetWebFeedRequest.IdentifierOneofCase.Id:
|
||||
if (!string.IsNullOrWhiteSpace(request.Id) && Guid.TryParse(request.Id, out var id))
|
||||
feed = await service.GetFeedAsync(id);
|
||||
break;
|
||||
case GetWebFeedRequest.IdentifierOneofCase.Url:
|
||||
feed = await db.WebFeeds.FirstOrDefaultAsync(f => f.Url == request.Url);
|
||||
break;
|
||||
case GetWebFeedRequest.IdentifierOneofCase.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return feed == null
|
||||
? throw new RpcException(new Status(StatusCode.NotFound, "feed not found"))
|
||||
: new GetWebFeedResponse { Feed = feed.ToProtoValue() };
|
||||
}
|
||||
|
||||
public override async Task<ListWebFeedsResponse> ListWebFeeds(
|
||||
ListWebFeedsRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (!Guid.TryParse(request.PublisherId, out var publisherId))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid publisher_id"));
|
||||
|
||||
var feeds = await service.GetFeedsByPublisherAsync(publisherId);
|
||||
|
||||
var response = new ListWebFeedsResponse
|
||||
{
|
||||
TotalSize = feeds.Count
|
||||
};
|
||||
response.Feeds.AddRange(feeds.Select(f => f.ToProtoValue()));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/feeds")]
|
||||
@@ -39,7 +40,7 @@ public class WebFeedPublicController(
|
||||
return Ok(existingSubscription);
|
||||
|
||||
// Create new subscription
|
||||
var subscription = new WebFeedSubscription
|
||||
var subscription = new SnWebFeedSubscription
|
||||
{
|
||||
FeedId = feedId,
|
||||
AccountId = accountId
|
||||
@@ -83,7 +84,7 @@ public class WebFeedPublicController(
|
||||
/// <returns>Subscription status</returns>
|
||||
[HttpGet("{feedId:guid}/subscription")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeedSubscription>> GetSubscriptionStatus(Guid feedId)
|
||||
public async Task<ActionResult<SnWebFeedSubscription>> GetSubscriptionStatus(Guid feedId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
@@ -105,7 +106,7 @@ public class WebFeedPublicController(
|
||||
/// <returns>List of subscribed feeds</returns>
|
||||
[HttpGet("subscribed")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeed>> GetSubscribedFeeds(
|
||||
public async Task<ActionResult<SnWebFeed>> GetSubscribedFeeds(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20
|
||||
)
|
||||
@@ -118,7 +119,6 @@ public class WebFeedPublicController(
|
||||
var query = db.WebFeedSubscriptions
|
||||
.Where(s => s.AccountId == accountId)
|
||||
.Include(s => s.Feed)
|
||||
.ThenInclude(f => f.Publisher)
|
||||
.OrderByDescending(s => s.CreatedAt);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
@@ -137,7 +137,7 @@ public class WebFeedPublicController(
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeed>> GetWebFeedArticles(
|
||||
public async Task<ActionResult<SnWebFeed>> GetWebFeedArticles(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20
|
||||
)
|
||||
@@ -154,7 +154,6 @@ public class WebFeedPublicController(
|
||||
|
||||
var query = db.WebFeeds
|
||||
.Where(f => subscribedFeedIds.Contains(f.Id))
|
||||
.Include(f => f.Publisher)
|
||||
.OrderByDescending(f => f.CreatedAt);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
@@ -174,7 +173,7 @@ public class WebFeedPublicController(
|
||||
/// <returns>Feed metadata</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{feedId:guid}")]
|
||||
public async Task<ActionResult<WebFeed>> GetFeedById(Guid feedId)
|
||||
public async Task<ActionResult<SnWebFeed>> GetFeedById(Guid feedId)
|
||||
{
|
||||
var feed = await webFeed.GetFeedAsync(feedId);
|
||||
if (feed == null)
|
||||
@@ -192,7 +191,7 @@ public class WebFeedPublicController(
|
||||
/// <returns>List of articles from the feed</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{feedId:guid}/articles")]
|
||||
public async Task<ActionResult<WebArticle>> GetFeedArticles(
|
||||
public async Task<ActionResult<SnWebArticle>> GetFeedArticles(
|
||||
[FromRoute] Guid feedId,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20
|
||||
@@ -206,8 +205,7 @@ public class WebFeedPublicController(
|
||||
var query = db.WebArticles
|
||||
.Where(a => a.FeedId == feedId)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.Include(a => a.Feed)
|
||||
.ThenInclude(f => f.Publisher);
|
||||
.Include(a => a.Feed);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var articles = await query
|
||||
@@ -224,7 +222,7 @@ public class WebFeedPublicController(
|
||||
/// </summary>
|
||||
[HttpGet("explore")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<WebFeed>> ExploreFeeds(
|
||||
public async Task<ActionResult<SnWebFeed>> ExploreFeeds(
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] string? query = null
|
||||
@@ -236,7 +234,6 @@ public class WebFeedPublicController(
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var feedsQuery = db.WebFeeds
|
||||
.Include(f => f.Publisher)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.AsQueryable();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public class WebFeedScraperJob(
|
||||
@@ -15,7 +16,7 @@ public class WebFeedScraperJob(
|
||||
{
|
||||
logger.LogInformation("Starting web feed scraper job.");
|
||||
|
||||
var feeds = await database.Set<WebFeed>().ToListAsync(context.CancellationToken);
|
||||
var feeds = await database.Set<SnWebFeed>().ToListAsync(context.CancellationToken);
|
||||
|
||||
foreach (var feed in feeds)
|
||||
{
|
||||
323
DysonNetwork.Insight/Reader/WebFeedService.cs
Normal file
323
DysonNetwork.Insight/Reader/WebFeedService.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.ServiceModel.Syndication;
|
||||
using System.Xml;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
public class WebFeedService(
|
||||
AppDatabase database,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<WebFeedService> logger,
|
||||
WebReaderService readerService,
|
||||
RemotePublisherService remotePublisherService
|
||||
)
|
||||
{
|
||||
private const string VerificationFileName = "solar-network-feed.txt";
|
||||
private static readonly TimeZoneInfo UtcZone = TimeZoneInfo.Utc;
|
||||
|
||||
public async Task<SnWebFeed> CreateWebFeedAsync(SnPublisher publisher, WebFeedController.WebFeedRequest request)
|
||||
{
|
||||
var feed = new SnWebFeed
|
||||
{
|
||||
Url = request.Url!,
|
||||
Title = request.Title!,
|
||||
Description = request.Description,
|
||||
Config = request.Config ?? new WebFeedConfig(),
|
||||
PublisherId = publisher.Id,
|
||||
Publisher = publisher
|
||||
};
|
||||
|
||||
database.WebFeeds.Add(feed);
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
private async Task<SnPublisher?> LoadPublisherAsync(Guid publisherId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await remotePublisherService.GetPublisher(id: publisherId.ToString(), cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (Grpc.Core.RpcException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SnWebFeed?> GetFeedAsync(Guid id, Guid? publisherId = null)
|
||||
{
|
||||
var query = database.WebFeeds
|
||||
.Where(a => a.Id == id)
|
||||
.AsQueryable();
|
||||
if (publisherId.HasValue)
|
||||
query = query.Where(a => a.PublisherId == publisherId.Value);
|
||||
var feed = await query.FirstOrDefaultAsync();
|
||||
if (feed != null)
|
||||
{
|
||||
feed.Publisher = await LoadPublisherAsync(feed.PublisherId, CancellationToken.None) ?? new SnPublisher();
|
||||
}
|
||||
return feed;
|
||||
}
|
||||
|
||||
public async Task<List<SnWebFeed>> GetFeedsByPublisherAsync(Guid publisherId)
|
||||
{
|
||||
var feeds = await database.WebFeeds.Where(a => a.PublisherId == publisherId).ToListAsync();
|
||||
foreach (var feed in feeds)
|
||||
{
|
||||
feed.Publisher = await LoadPublisherAsync(feed.PublisherId, CancellationToken.None) ?? new SnPublisher();
|
||||
}
|
||||
return feeds;
|
||||
}
|
||||
|
||||
public async Task<SnWebFeed> UpdateFeedAsync(SnWebFeed feed, WebFeedController.WebFeedRequest request)
|
||||
{
|
||||
if (request.Url is not null)
|
||||
feed.Url = request.Url;
|
||||
if (request.Title is not null)
|
||||
feed.Title = request.Title;
|
||||
if (request.Description is not null)
|
||||
feed.Description = request.Description;
|
||||
if (request.Config is not null)
|
||||
feed.Config = request.Config;
|
||||
|
||||
database.Update(feed);
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
feed.Publisher = await LoadPublisherAsync(feed.PublisherId, CancellationToken.None) ?? new SnPublisher();
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFeedAsync(Guid id)
|
||||
{
|
||||
var feed = await database.WebFeeds.FindAsync(id);
|
||||
if (feed == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
database.WebFeeds.Remove(feed);
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task ScrapeFeedAsync(SnWebFeed feed, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
var response = await httpClient.GetAsync(feed.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = XmlReader.Create(stream);
|
||||
var syndicationFeed = SyndicationFeed.Load(reader);
|
||||
|
||||
if (syndicationFeed == null)
|
||||
{
|
||||
logger.LogWarning("Could not parse syndication feed for {FeedUrl}", feed.Url);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in syndicationFeed.Items)
|
||||
{
|
||||
var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString();
|
||||
if (string.IsNullOrEmpty(itemUrl))
|
||||
continue;
|
||||
|
||||
var articleExists = await database.Set<SnWebArticle>()
|
||||
.AnyAsync(a => a.FeedId == feed.Id && a.Url == itemUrl, cancellationToken);
|
||||
|
||||
if (articleExists)
|
||||
continue;
|
||||
|
||||
var content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text;
|
||||
LinkEmbed preview;
|
||||
|
||||
if (feed.Config.ScrapPage)
|
||||
{
|
||||
var scrapedArticle = await readerService.ScrapeArticleAsync(itemUrl, cancellationToken);
|
||||
preview = scrapedArticle.LinkEmbed;
|
||||
if (scrapedArticle.Content is not null)
|
||||
content = scrapedArticle.Content;
|
||||
}
|
||||
else
|
||||
{
|
||||
preview = await readerService.GetLinkPreviewAsync(itemUrl, cancellationToken);
|
||||
}
|
||||
|
||||
var newArticle = new SnWebArticle
|
||||
{
|
||||
FeedId = feed.Id,
|
||||
Title = item.Title.Text,
|
||||
Url = itemUrl,
|
||||
Author = item.Authors.FirstOrDefault()?.Name,
|
||||
Content = content,
|
||||
PublishedAt = item.LastUpdatedTime.UtcDateTime,
|
||||
Preview = preview,
|
||||
};
|
||||
|
||||
database.WebArticles.Add(newArticle);
|
||||
}
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<WebFeedVerificationInitResult> GenerateVerificationCodeAsync(Guid feedId)
|
||||
{
|
||||
var feed = await database.WebFeeds.FindAsync(feedId);
|
||||
if (feed == null)
|
||||
throw new InvalidOperationException($"Feed with ID {feedId} not found");
|
||||
|
||||
var domain = GetDomainFromUrl(feed.Url);
|
||||
var verificationCode = GenerateVerificationCode();
|
||||
var verificationUrl = $"https://{domain}/.well-known/{VerificationFileName}";
|
||||
|
||||
feed.VerificationKey = verificationCode;
|
||||
await database.SaveChangesAsync();
|
||||
|
||||
return new WebFeedVerificationInitResult
|
||||
{
|
||||
VerificationUrl = verificationUrl,
|
||||
Code = verificationCode,
|
||||
Instructions = $"Create a file at '{verificationUrl}' containing only this verification code."
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WebFeedVerificationResult> VerifyOwnershipAsync(Guid feedId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var feed = await database.WebFeeds.FindAsync(feedId);
|
||||
if (feed == null)
|
||||
throw new InvalidOperationException($"Feed with ID {feedId} not found");
|
||||
|
||||
if (string.IsNullOrEmpty(feed.VerificationKey))
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "No verification code generated. Please call the init endpoint first."
|
||||
};
|
||||
|
||||
var domain = GetDomainFromUrl(feed.Url);
|
||||
var verificationUrl = $"https://{domain}/.well-known/{VerificationFileName}";
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
var response = await httpClient.GetAsync(verificationUrl, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await RevokeVerificationAsync(feed, "Verification file not found or inaccessible");
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Verification file not found (HTTP {response.StatusCode}). Verification status has been revoked."
|
||||
};
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var trimmedContent = content.Trim();
|
||||
|
||||
if (trimmedContent != feed.VerificationKey)
|
||||
{
|
||||
await RevokeVerificationAsync(feed, "Verification code mismatch");
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "Verification code does not match. Verification status has been revoked."
|
||||
};
|
||||
}
|
||||
|
||||
feed.VerifiedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
feed.VerificationKey = null;
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Successfully verified ownership of feed {FeedId} at {Url}", feedId, feed.Url);
|
||||
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = true,
|
||||
VerifiedAt = feed.VerifiedAt.Value.ToDateTimeUtc(),
|
||||
Message = "Website ownership verified successfully."
|
||||
};
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
await RevokeVerificationAsync(feed, "Verification request timed out");
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Message = "Verification request timed out. Verification status has been revoked."
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error during verification for feed {FeedId}", feedId);
|
||||
await RevokeVerificationAsync(feed, $"Verification error: {ex.Message}");
|
||||
return new WebFeedVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Error during verification: {ex.Message}. Verification status has been revoked."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevokeVerificationAsync(SnWebFeed feed, string reason)
|
||||
{
|
||||
logger.LogWarning("Revoking verification for feed {FeedId}: {Reason}", feed.Id, reason);
|
||||
feed.VerifiedAt = null;
|
||||
feed.VerificationKey = null;
|
||||
await database.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task VerifyAllFeedsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var verifiedFeeds = await database.WebFeeds
|
||||
.Where(f => f.VerifiedAt.HasValue)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Starting periodic verification check for {Count} feeds", verifiedFeeds.Count);
|
||||
|
||||
foreach (var feed in verifiedFeeds)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
await VerifyOwnershipAsync(feed.Id, cancellationToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("Completed periodic verification check");
|
||||
}
|
||||
|
||||
private static string GenerateVerificationCode()
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd");
|
||||
var randomPart = Guid.NewGuid().ToString("N")[..16];
|
||||
return $"dn_{timestamp}_{randomPart}";
|
||||
}
|
||||
|
||||
private static string GetDomainFromUrl(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
return uri.Host;
|
||||
}
|
||||
}
|
||||
|
||||
public class WebFeedVerificationInitResult
|
||||
{
|
||||
public string VerificationUrl { get; set; } = string.Empty;
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Instructions { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class WebFeedVerificationResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public DateTime? VerifiedAt { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
28
DysonNetwork.Insight/Reader/WebFeedVerificationJob.cs
Normal file
28
DysonNetwork.Insight/Reader/WebFeedVerificationJob.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
public class WebFeedVerificationJob(
|
||||
WebFeedService webFeedService,
|
||||
ILogger<WebFeedVerificationJob> logger
|
||||
)
|
||||
: IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
logger.LogInformation("Starting web feed verification job.");
|
||||
|
||||
try
|
||||
{
|
||||
await webFeedService.VerifyAllFeedsAsync(context.CancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error during web feed verification job");
|
||||
}
|
||||
|
||||
logger.LogInformation("Web feed verification job finished.");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for web scraping and link preview services
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an error occurs during web reading operations
|
||||
49
DysonNetwork.Insight/Reader/WebReaderGrpcService.cs
Normal file
49
DysonNetwork.Insight/Reader/WebReaderGrpcService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
public class WebReaderGrpcService(WebReaderService service) : Shared.Proto.WebReaderService.WebReaderServiceBase
|
||||
{
|
||||
public override async Task<ScrapeArticleResponse> ScrapeArticle(
|
||||
ScrapeArticleRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "url is required"));
|
||||
|
||||
var scrapedArticle = await service.ScrapeArticleAsync(request.Url, context.CancellationToken);
|
||||
return new ScrapeArticleResponse { Article = scrapedArticle.ToProtoValue() };
|
||||
}
|
||||
|
||||
public override async Task<GetLinkPreviewResponse> GetLinkPreview(
|
||||
GetLinkPreviewRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "url is required"));
|
||||
|
||||
var linkEmbed = await service.GetLinkPreviewAsync(
|
||||
request.Url,
|
||||
context.CancellationToken,
|
||||
bypassCache: request.BypassCache
|
||||
);
|
||||
|
||||
return new GetLinkPreviewResponse { Preview = linkEmbed.ToProtoValue() };
|
||||
}
|
||||
|
||||
public override async Task<InvalidateLinkPreviewCacheResponse> InvalidateLinkPreviewCache(
|
||||
InvalidateLinkPreviewCacheRequest request,
|
||||
ServerCallContext context
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "url is required"));
|
||||
|
||||
await service.InvalidateCacheForUrlAsync(request.Url);
|
||||
|
||||
return new InvalidateLinkPreviewCacheResponse { Success = true };
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ using System.Globalization;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace DysonNetwork.Sphere.WebReader;
|
||||
namespace DysonNetwork.Insight.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// The service is amin to providing scrapping service to the Solar Network.
|
||||
@@ -1,4 +1,5 @@
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Insight.Reader;
|
||||
using DysonNetwork.Shared.Networking;
|
||||
|
||||
namespace DysonNetwork.Insight.Startup;
|
||||
|
||||
@@ -17,6 +18,11 @@ public static class ApplicationConfiguration
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGrpcService<WebReaderGrpcService>();
|
||||
app.MapGrpcService<WebArticleGrpcService>();
|
||||
app.MapGrpcService<WebFeedGrpcService>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Insight.Reader;
|
||||
using DysonNetwork.Insight.Thought;
|
||||
using Quartz;
|
||||
|
||||
@@ -18,6 +19,20 @@ public static class ScheduledJobsConfiguration
|
||||
.WithIntervalInMinutes(5)
|
||||
.RepeatForever())
|
||||
);
|
||||
|
||||
q.AddJob<WebFeedScraperJob>(opts => opts.WithIdentity("WebFeedScraper").StoreDurably());
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob("WebFeedScraper")
|
||||
.WithIdentity("WebFeedScraperTrigger")
|
||||
.WithCronSchedule("0 0 0 * * ?")
|
||||
);
|
||||
|
||||
q.AddJob<WebFeedVerificationJob>(opts => opts.WithIdentity("WebFeedVerification").StoreDurably());
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob("WebFeedVerification")
|
||||
.WithIdentity("WebFeedVerificationTrigger")
|
||||
.WithCronSchedule("0 0 4 * * ?")
|
||||
);
|
||||
});
|
||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Insight.Reader;
|
||||
using DysonNetwork.Insight.Thought;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.SemanticKernel;
|
||||
using NodaTime;
|
||||
@@ -11,60 +13,65 @@ namespace DysonNetwork.Insight.Startup;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
||||
extension(IServiceCollection services)
|
||||
{
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
// Register gRPC services
|
||||
services.AddGrpc(options =>
|
||||
public IServiceCollection AddAppServices()
|
||||
{
|
||||
options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
services.AddGrpcReflection();
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
// Register gRPC services
|
||||
services.AddHttpClient();
|
||||
|
||||
// Register OIDC services
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
// Register gRPC services
|
||||
services.AddGrpc(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
services.AddGrpcReflection();
|
||||
|
||||
// Register gRPC services
|
||||
|
||||
// Register OIDC services
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public IServiceCollection AddAppAuthentication()
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
public IServiceCollection AddAppFlushHandlers()
|
||||
{
|
||||
services.AddSingleton<FlushBufferService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
public IServiceCollection AddAppBusinessServices()
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<FlushBufferService>();
|
||||
public IServiceCollection AddThinkingServices(IConfiguration configuration)
|
||||
{
|
||||
services.AddSingleton<ThoughtProvider>();
|
||||
services.AddScoped<ThoughtService>();
|
||||
services.AddScoped<Reader.WebFeedService>();
|
||||
services.AddScoped<Reader.WebReaderService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services)
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddThinkingServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddSingleton<ThoughtProvider>();
|
||||
services.AddScoped<ThoughtService>();
|
||||
|
||||
return services;
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace DysonNetwork.Insight.Thought.Plugins;
|
||||
@@ -24,6 +23,6 @@ public class SnAccountKernelPlugin(
|
||||
var request = new LookupAccountBatchRequest();
|
||||
request.Names.Add(username);
|
||||
var response = await accountClient.LookupAccountBatchAsync(request);
|
||||
return response.Accounts.IsNullOrEmpty() ? null : SnAccount.FromProtoValue(response.Accounts[0]);
|
||||
return response.Accounts.Count == 0 ? null : SnAccount.FromProtoValue(response.Accounts[0]);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"Insecure": true
|
||||
},
|
||||
"Cache": {
|
||||
"Serializer": "MessagePack"
|
||||
"Serializer": "JSON"
|
||||
},
|
||||
"Thinking": {
|
||||
"DefaultService": "deepseek-chat",
|
||||
|
||||
5
DysonNetwork.Messager/.gitignore
vendored
Normal file
5
DysonNetwork.Messager/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Keys
|
||||
Uploads
|
||||
DataProtection-Keys
|
||||
|
||||
.DS_Store
|
||||
139
DysonNetwork.Messager/AppDatabase.cs
Normal file
139
DysonNetwork.Messager/AppDatabase.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Linq.Expressions;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Messager;
|
||||
|
||||
public class AppDatabase(
|
||||
DbContextOptions<AppDatabase> options,
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<SnChatRoom> ChatRooms { get; set; } = null!;
|
||||
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
||||
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
||||
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
||||
public DbSet<SnChatReaction> ChatReactions { get; set; } = null!;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
configuration.GetConnectionString("App"),
|
||||
opt => opt
|
||||
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
.UseNodaTime()
|
||||
).UseSnakeCaseNamingConvention();
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<SnChatMember>()
|
||||
.HasKey(pm => new { pm.Id });
|
||||
modelBuilder.Entity<SnChatMember>()
|
||||
.HasAlternateKey(pm => new { pm.ChatRoomId, pm.AccountId });
|
||||
modelBuilder.Entity<SnChatMember>()
|
||||
.HasOne(pm => pm.ChatRoom)
|
||||
.WithMany(p => p.Members)
|
||||
.HasForeignKey(pm => pm.ChatRoomId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<SnChatMessage>()
|
||||
.HasOne(m => m.ForwardedMessage)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.ForwardedMessageId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<SnChatMessage>()
|
||||
.HasOne(m => m.RepliedMessage)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.RepliedMessageId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<SnRealtimeCall>()
|
||||
.HasOne(m => m.Room)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.RoomId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<SnRealtimeCall>()
|
||||
.HasOne(m => m.Sender)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.SenderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ApplyAuditableAndSoftDelete();
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> logger) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
logger.LogInformation("Deleting soft-deleted records...");
|
||||
|
||||
var threshold = now - Duration.FromDays(7);
|
||||
|
||||
var entityTypes = db.Model.GetEntityTypes()
|
||||
.Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase))
|
||||
.Select(t => t.ClrType);
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
|
||||
.MakeGenericMethod(entityType).Invoke(db, null)!;
|
||||
var parameter = Expression.Parameter(entityType, "e");
|
||||
var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt));
|
||||
var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?)));
|
||||
var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?)));
|
||||
var finalCondition = Expression.AndAlso(notNull, condition);
|
||||
var lambda = Expression.Lambda(finalCondition, parameter);
|
||||
|
||||
var queryable = set.Provider.CreateQuery(
|
||||
Expression.Call(
|
||||
typeof(Queryable),
|
||||
"Where",
|
||||
[entityType],
|
||||
set.Expression,
|
||||
Expression.Quote(lambda)
|
||||
)
|
||||
);
|
||||
|
||||
var toListAsync = typeof(EntityFrameworkQueryableExtensions)
|
||||
.GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))!
|
||||
.MakeGenericMethod(entityType);
|
||||
|
||||
var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!;
|
||||
db.RemoveRange(items);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||
{
|
||||
public AppDatabase CreateDbContext(string[] args)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
|
||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,16 @@ using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Autocompletion;
|
||||
using DysonNetwork.Sphere.Poll;
|
||||
using DysonNetwork.Sphere.Wallet;
|
||||
using DysonNetwork.Sphere.WebReader;
|
||||
using DysonNetwork.Messager.Poll;
|
||||
using DysonNetwork.Messager.Wallet;
|
||||
using DysonNetwork.Shared.Models.Embed;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
namespace DysonNetwork.Messager.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/chat")]
|
||||
@@ -24,9 +23,8 @@ public partial class ChatController(
|
||||
ChatRoomService crs,
|
||||
FileService.FileServiceClient files,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
AutocompletionService aus,
|
||||
PaymentService.PaymentServiceClient paymentClient,
|
||||
PollService polls
|
||||
PollService.PollServiceClient pollClient
|
||||
) : ControllerBase
|
||||
{
|
||||
public class MarkMessageReadRequest
|
||||
@@ -292,12 +290,16 @@ public partial class ChatController(
|
||||
{
|
||||
try
|
||||
{
|
||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
||||
// Poll validation is handled by the MakePollEmbed method
|
||||
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||
// Poll validation is handled by gRPC call
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest("The specified poll does not exist.");
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||
{
|
||||
return BadRequest("Invalid poll ID.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,12 +330,13 @@ public partial class ChatController(
|
||||
// Add embed for poll if provided
|
||||
if (request.PollId.HasValue)
|
||||
{
|
||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
||||
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||
var pollEmbed = new PollEmbed { Id = Guid.Parse(pollResponse.Id) };
|
||||
message.Meta ??= new Dictionary<string, object>();
|
||||
if (
|
||||
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
||||
|| existingEmbeds is not List<EmbeddableBase>
|
||||
)
|
||||
|| existingEmbeds is not List<EmbeddableBase>
|
||||
)
|
||||
message.Meta["embeds"] = new List<Dictionary<string, object>>();
|
||||
var embeds = (List<Dictionary<string, object>>)message.Meta["embeds"];
|
||||
embeds.Add(EmbeddableBase.ToDictionary(pollEmbed));
|
||||
@@ -471,7 +474,8 @@ public partial class ChatController(
|
||||
{
|
||||
try
|
||||
{
|
||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
||||
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||
var pollEmbed = new PollEmbed { Id = Guid.Parse(pollResponse.Id) };
|
||||
message.Meta ??= new Dictionary<string, object>();
|
||||
if (
|
||||
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
||||
@@ -486,9 +490,13 @@ public partial class ChatController(
|
||||
embeds.Add(EmbeddableBase.ToDictionary(pollEmbed));
|
||||
message.Meta["embeds"] = embeds;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest("The specified poll does not exist.");
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||
{
|
||||
return BadRequest("Invalid poll ID.");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -564,20 +572,4 @@ public partial class ChatController(
|
||||
}
|
||||
|
||||
|
||||
public async Task<ActionResult<List<Shared.Models.Autocompletion>>> ChatAutoComplete(
|
||||
[FromBody] AutocompletionRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var isMember = await db.ChatMembers
|
||||
.AnyAsync(m =>
|
||||
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
||||
if (!isMember)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
|
||||
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,13 @@ using DysonNetwork.Shared;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
namespace DysonNetwork.Messager.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/chat")]
|
||||
@@ -20,10 +19,8 @@ public class ChatRoomController(
|
||||
AppDatabase db,
|
||||
ChatRoomService crs,
|
||||
RemoteRealmService rs,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
FileService.FileServiceClient files,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
RingService.RingServiceClient pusher,
|
||||
RemoteAccountService remoteAccountsHelper
|
||||
@@ -42,6 +39,8 @@ public class ChatRoomController(
|
||||
|
||||
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
||||
|
||||
chatRoom = await crs.LoadChatRealms(chatRoom);
|
||||
|
||||
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
||||
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, Guid.Parse(currentUser.Id));
|
||||
|
||||
@@ -62,6 +61,7 @@ public class ChatRoomController(
|
||||
.Include(m => m.ChatRoom)
|
||||
.Select(m => m.ChatRoom)
|
||||
.ToListAsync();
|
||||
chatRooms = await crs.LoadChatRealms(chatRooms);
|
||||
chatRooms = await crs.LoadDirectMessageMembers(chatRooms, accountId);
|
||||
chatRooms = await crs.SortChatRoomByLastMessage(chatRooms);
|
||||
|
||||
@@ -219,13 +219,6 @@ public class ChatRoomController(
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
chatRoom.Picture = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chatroom.picture",
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
});
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
@@ -240,13 +233,6 @@ public class ChatRoomController(
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
chatRoom.Background = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chatroom.background",
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
});
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
@@ -257,28 +243,6 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Add(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
||||
|
||||
if (chatRoom.Picture is not null)
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = chatRoom.Picture.Id,
|
||||
Usage = "chat.room.picture",
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
}
|
||||
|
||||
if (chatRoom.Background is not null)
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = chatRoom.Background.Id,
|
||||
Usage = "chat.room.background",
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
}
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.create",
|
||||
@@ -308,7 +272,7 @@ public class ChatRoomController(
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||
return StatusCode(403, "You need at least be a realm moderator to update the chat.");
|
||||
}
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && !await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
return StatusCode(403, "You need be part of the DM to update the chat.");
|
||||
else if (chatRoom.AccountId != accountId)
|
||||
return StatusCode(403, "You need be the owner to update the chat.");
|
||||
@@ -328,21 +292,6 @@ public class ChatRoomController(
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for pictures
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
Usage = "chat.room.picture"
|
||||
});
|
||||
|
||||
// Add a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chat.room.picture",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
});
|
||||
|
||||
chatRoom.Picture = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
@@ -358,21 +307,6 @@ public class ChatRoomController(
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for backgrounds
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
Usage = "chat.room.background"
|
||||
});
|
||||
|
||||
// Add a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chat.room.background",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
});
|
||||
|
||||
chatRoom.Background = SnCloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
@@ -421,19 +355,11 @@ public class ChatRoomController(
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||
return StatusCode(403, "You need at least be a realm moderator to delete the chat.");
|
||||
}
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && !await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
return StatusCode(403, "You need be part of the DM to update the chat.");
|
||||
else if (chatRoom.AccountId != accountId)
|
||||
return StatusCode(403, "You need be the owner to update the chat.");
|
||||
|
||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
||||
|
||||
// Delete all file references for this chat room
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
@@ -522,13 +448,15 @@ public class ChatRoomController(
|
||||
}
|
||||
|
||||
[HttpGet("{roomId:guid}/members")]
|
||||
public async Task<ActionResult<List<SnChatMember>>> ListMembers(Guid roomId,
|
||||
public async Task<ActionResult<List<SnChatMember>>> ListMembers(
|
||||
Guid roomId,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] bool withStatus = false
|
||||
)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
Guid? accountId = currentUser is not null ? Guid.Parse(currentUser.Id) : null;
|
||||
|
||||
var room = await db.ChatRooms
|
||||
.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
@@ -536,16 +464,17 @@ public class ChatRoomController(
|
||||
|
||||
if (!room.IsPublic)
|
||||
{
|
||||
if (currentUser is null) return Unauthorized();
|
||||
if (accountId is null) return Unauthorized();
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id) && m.LeaveAt == null)
|
||||
.Where(m => m.ChatRoomId == roomId && m.AccountId == accountId && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room.");
|
||||
}
|
||||
|
||||
// The query should include the unjoined ones, to show the invites.
|
||||
var query = db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null);
|
||||
.Where(m => m.LeaveAt == null);
|
||||
|
||||
if (withStatus)
|
||||
{
|
||||
@@ -638,7 +567,7 @@ public class ChatRoomController(
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||
return StatusCode(403, "You need at least be a realm moderator to invite members to this chat.");
|
||||
}
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && !await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
return StatusCode(403, "You need be part of the DM to invite member to the chat.");
|
||||
else if (chatRoom.AccountId != accountId)
|
||||
return StatusCode(403, "You need be the owner to invite member to this chat.");
|
||||
@@ -715,8 +644,10 @@ public class ChatRoomController(
|
||||
.ToListAsync();
|
||||
|
||||
var chatRooms = members.Select(m => m.ChatRoom).ToList();
|
||||
chatRooms = await crs.LoadDirectMessageMembers(chatRooms, accountId);
|
||||
chatRooms = await crs.LoadChatRealms(chatRooms);
|
||||
var directMembers =
|
||||
(await crs.LoadDirectMessageMembers(chatRooms, accountId)).ToDictionary(c => c.Id, c => c.Members);
|
||||
chatRooms.ToDictionary(c => c.Id, c => c.Members);
|
||||
|
||||
foreach (var member in members.Where(member => member.ChatRoom.Type == ChatRoomType.DirectMessage))
|
||||
member.ChatRoom.Members = directMembers[member.ChatRoom.Id];
|
||||
@@ -902,7 +833,7 @@ public class ChatRoomController(
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
||||
}
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && !await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
return StatusCode(403, "You need be part of the DM to update the chat.");
|
||||
else if (chatRoom.AccountId != accountId)
|
||||
return StatusCode(403, "You need be the owner to update the chat.");
|
||||
@@ -953,7 +884,7 @@ public class ChatRoomController(
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
||||
}
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
else if (chatRoom.Type == ChatRoomType.DirectMessage && !await crs.IsChatMember(chatRoom.Id, accountId))
|
||||
return StatusCode(403, "You need be part of the DM to update the chat.");
|
||||
else if (chatRoom.AccountId != accountId)
|
||||
return StatusCode(403, "You need be the owner to update the chat.");
|
||||
@@ -1076,12 +1007,11 @@ public class ChatRoomController(
|
||||
{
|
||||
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
||||
CultureService.SetCultureInfo(account);
|
||||
var title = "Chat Invite";
|
||||
var body = member.ChatRoom.Type == ChatRoomType.DirectMessage
|
||||
? $"{sender.Nick} sent you a direct message"
|
||||
: $"You have been invited to {member.ChatRoom.Name ?? "Unnamed"}";
|
||||
|
||||
string title = localizer["ChatInviteTitle"];
|
||||
|
||||
string body = member.ChatRoom.Type == ChatRoomType.DirectMessage
|
||||
? localizer["ChatInviteDirectBody", sender.Nick]
|
||||
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
||||
|
||||
await pusher.SendPushNotificationToUserAsync(
|
||||
new SendPushNotificationToUserRequest
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user