5 Commits

Author SHA1 Message Date
24c756a9a8 🗑️ Remove gateway got replaced by turbine one 2025-12-13 19:49:56 +08:00
7ecb64742f ♻️ Updated discovery resolver 2025-12-13 19:28:24 +08:00
3a7140f0a6 ♻️ Update service discovery code 2025-12-13 18:52:55 +08:00
42082fbefa 🔨 Reconfigured to use new discovery 2025-12-13 17:38:49 +08:00
bc3d030a1e New service discovery system 2025-12-13 14:23:28 +08:00
405 changed files with 46944 additions and 35848 deletions

View File

@@ -1,43 +0,0 @@
# 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

View File

@@ -1,103 +1,103 @@
name: Build and Push Microservices name: Build and Push Microservices
on: on:
push: push:
branches: branches:
- master - master
workflow_dispatch: workflow_dispatch:
jobs: jobs:
determine-changes: determine-changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
matrix: ${{ steps.changes.outputs.matrix }} matrix: ${{ steps.changes.outputs.matrix }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
run: | run: |
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT
- name: Determine changed services - name: Determine changed services
id: changes id: changes
run: | run: |
files="${{ steps.changed-files.outputs.files }}" files="${{ steps.changed-files.outputs.files }}"
matrix="{\"include\":[]}" matrix="{\"include\":[]}"
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone" "Messager") services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone")
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone" "messager") images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone")
changed_services=() changed_services=()
for file in $files; do for file in $files; do
if [[ "$file" == DysonNetwork.Shared/* ]]; then if [[ "$file" == DysonNetwork.Shared/* ]]; then
changed_services=("${services[@]}") changed_services=("${services[@]}")
break break
fi fi
for i in "${!services[@]}"; do for i in "${!services[@]}"; do
if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then
# check if service is already in changed_services # check if service is already in changed_services
if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then
changed_services+=("${services[$i]}") changed_services+=("${services[$i]}")
fi fi
fi fi
done done
done done
if [ ${#changed_services[@]} -gt 0 ]; then if [ ${#changed_services[@]} -gt 0 ]; then
json_objects="" json_objects=""
for service in "${changed_services[@]}"; do for service in "${changed_services[@]}"; do
for i in "${!services[@]}"; do for i in "${!services[@]}"; do
if [[ "${services[$i]}" == "$service" ]]; then if [[ "${services[$i]}" == "$service" ]]; then
image="${images[$i]}" image="${images[$i]}"
break break
fi fi
done done
json_objects+="{\"service\":\"$service\",\"image\":\"$image\"}," json_objects+="{\"service\":\"$service\",\"image\":\"$image\"},"
done done
matrix="{\"include\":[${json_objects%,}]}" matrix="{\"include\":[${json_objects%,}]}"
fi fi
echo "matrix=$matrix" >> $GITHUB_OUTPUT echo "matrix=$matrix" >> $GITHUB_OUTPUT
build-and-push: build-and-push:
needs: determine-changes needs: determine-changes
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }} if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
strategy: strategy:
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }} matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup NBGV - name: Setup NBGV
uses: dotnet/nbgv@master uses: dotnet/nbgv@master
id: nbgv id: nbgv
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image for ${{ matrix.service }} - name: Build and push Docker image for ${{ matrix.service }}
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: DysonNetwork.${{ matrix.service }}/Dockerfile file: DysonNetwork.${{ matrix.service }}/Dockerfile
push: true push: true
tags: | tags: |
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }} ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
platforms: linux/amd64 platforms: linux/amd64

613
API_WALLET_FUNDS.md Normal file
View File

@@ -0,0 +1,613 @@
# 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

View File

@@ -1,92 +0,0 @@
using Microsoft.Extensions.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var isDev = builder.Environment.IsDevelopment();
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")
.WithReference(ringService);
var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
.WithReference(passService)
.WithReference(ringService);
var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
.WithReference(passService)
.WithReference(ringService)
.WithReference(driveService);
var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
.WithReference(passService)
.WithReference(ringService)
.WithReference(sphereService);
var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight")
.WithReference(passService)
.WithReference(ringService)
.WithReference(sphereService)
.WithReference(developService);
var zoneService = builder.AddProject<Projects.DysonNetwork_Zone>("zone")
.WithReference(passService)
.WithReference(ringService)
.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,
messagerService
];
for (var idx = 0; idx < services.Count; idx++)
{
var service = services[idx];
service.WithReference(cache).WithReference(queue);
var grpcPort = 7002 + idx;
if (isDev)
{
service.WithEnvironment("GRPC_PORT", grpcPort.ToString());
var httpPort = 8001 + idx;
service.WithEnvironment("HTTP_PORTS", httpPort.ToString());
service.WithHttpEndpoint(httpPort, targetPort: null, isProxied: false, name: "http");
}
else
{
service.WithHttpEndpoint(8080, targetPort: null, isProxied: false, name: "http");
}
service.WithEndpoint(isDev ? grpcPort : 7001, isDev ? null : 7001, "https", name: "grpc", isProxied: false);
}
// Extra double-ended references
ringService.WithReference(passService);
var gateway = builder.AddProject<Projects.DysonNetwork_Gateway>("gateway")
.WithEnvironment("HTTP_PORTS", "5001")
.WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http");
foreach (var service in services)
gateway.WithReference(service);
builder.AddDockerComposeEnvironment("docker-compose");
builder.Build().Run();

View File

@@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
<RootNamespace>DysonNetwork.Control</RootNamespace>
</PropertyGroup>
<ItemGroup>
<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.1.0"/>
<PackageReference Include="Aspire.Hosting.Redis" Version="13.1.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj"/>
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj"/>
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj"/>
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj"/>
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj"/>
<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>

View File

@@ -1,32 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17169;http://localhost:15057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21260",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22052"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108"
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"cache": "localhost:6379"
}
}

View File

@@ -1,357 +0,0 @@
{
"$schema": "https://json.schemastore.org/aspire-8.0.json",
"resources": {
"cache": {
"type": "container.v1",
"connectionString": "{cache.bindings.tcp.host}:{cache.bindings.tcp.port},password={cache-password.value}",
"image": "docker.io/library/redis:8.2",
"entrypoint": "/bin/sh",
"args": [
"-c",
"redis-server --requirepass $REDIS_PASSWORD"
],
"env": {
"REDIS_PASSWORD": "{cache-password.value}"
},
"bindings": {
"tcp": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 6379
}
}
},
"queue": {
"type": "container.v1",
"connectionString": "nats://nats:{queue-password.value}@{queue.bindings.tcp.host}:{queue.bindings.tcp.port}",
"image": "docker.io/library/nats:2.11",
"args": [
"--user",
"nats",
"--pass",
"{queue-password.value}",
"-js"
],
"bindings": {
"tcp": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 4222
}
}
},
"ring": {
"type": "project.v1",
"path": "../DysonNetwork.Ring/DysonNetwork.Ring.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8001",
"HTTPS_PORTS": "{ring.bindings.grpc.targetPort}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7002",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "ring"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8001
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7002
}
}
},
"pass": {
"type": "project.v1",
"path": "../DysonNetwork.Pass/DysonNetwork.Pass.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8002",
"HTTPS_PORTS": "{pass.bindings.grpc.targetPort}",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"services__develop__http__0": "{develop.bindings.http.url}",
"services__develop__grpc__0": "{develop.bindings.grpc.url}",
"services__drive__http__0": "{drive.bindings.http.url}",
"services__drive__grpc__0": "{drive.bindings.grpc.url}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7003",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "pass"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8002
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7003
}
}
},
"drive": {
"type": "project.v1",
"path": "../DysonNetwork.Drive/DysonNetwork.Drive.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8003",
"HTTPS_PORTS": "{drive.bindings.grpc.targetPort}",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7004",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "drive"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8003
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7004
}
}
},
"sphere": {
"type": "project.v1",
"path": "../DysonNetwork.Sphere/DysonNetwork.Sphere.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8004",
"HTTPS_PORTS": "{sphere.bindings.grpc.targetPort}",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"services__drive__http__0": "{drive.bindings.http.url}",
"services__drive__grpc__0": "{drive.bindings.grpc.url}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7005",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "sphere"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8004
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7005
}
}
},
"develop": {
"type": "project.v1",
"path": "../DysonNetwork.Develop/DysonNetwork.Develop.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8005",
"HTTPS_PORTS": "{develop.bindings.grpc.targetPort}",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"services__sphere__http__0": "{sphere.bindings.http.url}",
"services__sphere__grpc__0": "{sphere.bindings.grpc.url}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7006",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "develop"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8005
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7006
}
}
},
"insight": {
"type": "project.v1",
"path": "../DysonNetwork.Insight/DysonNetwork.Insight.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "8006",
"HTTPS_PORTS": "{insight.bindings.grpc.targetPort}",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"services__sphere__http__0": "{sphere.bindings.http.url}",
"services__sphere__grpc__0": "{sphere.bindings.grpc.url}",
"services__develop__http__0": "{develop.bindings.http.url}",
"services__develop__grpc__0": "{develop.bindings.grpc.url}",
"ConnectionStrings__cache": "{cache.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"GRPC_PORT": "7007",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "insight"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8006
},
"grpc": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 7007
}
}
},
"gateway": {
"type": "project.v1",
"path": "../DysonNetwork.Gateway/DysonNetwork.Gateway.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "5001",
"services__ring__http__0": "{ring.bindings.http.url}",
"services__ring__grpc__0": "{ring.bindings.grpc.url}",
"services__pass__http__0": "{pass.bindings.http.url}",
"services__pass__grpc__0": "{pass.bindings.grpc.url}",
"services__drive__http__0": "{drive.bindings.http.url}",
"services__drive__grpc__0": "{drive.bindings.grpc.url}",
"services__sphere__http__0": "{sphere.bindings.http.url}",
"services__sphere__grpc__0": "{sphere.bindings.grpc.url}",
"services__develop__http__0": "{develop.bindings.http.url}",
"services__develop__grpc__0": "{develop.bindings.grpc.url}",
"services__insight__http__0": "{insight.bindings.http.url}",
"services__insight__grpc__0": "{insight.bindings.grpc.url}",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{docker-compose-dashboard.bindings.otlp-grpc.url}",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_SERVICE_NAME": "gateway"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 5001
}
}
},
"docker-compose": {
"error": "This resource does not support generation in the manifest."
},
"cache-password": {
"type": "parameter.v0",
"value": "{cache-password.inputs.value}",
"inputs": {
"value": {
"type": "string",
"secret": true,
"default": {
"generate": {
"minLength": 22,
"special": false
}
}
}
}
},
"queue-password": {
"type": "parameter.v0",
"value": "{queue-password.inputs.value}",
"inputs": {
"value": {
"type": "string",
"secret": true,
"default": {
"generate": {
"minLength": 22,
"special": false
}
}
}
}
},
"docker-compose-dashboard": {
"type": "container.v1",
"image": "mcr.microsoft.com/dotnet/nightly/aspire-dashboard:latest",
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 18888
},
"otlp-grpc": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 18889
}
}
}
}
}

View File

@@ -18,7 +18,6 @@ public class AppDatabase(
public DbSet<SnCustomApp> CustomApps { get; set; } = null!; public DbSet<SnCustomApp> CustomApps { get; set; } = null!;
public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!; public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
public DbSet<SnBotAccount> BotAccounts { get; set; } = null!; public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
public DbSet<SnMiniApp> MiniApps { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

View File

@@ -1,9 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base 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 USER $APP_UID
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080

View File

@@ -8,15 +8,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime" Version="3.2.3" /> <PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" /> <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -29,8 +29,4 @@
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="MiniApp\" />
</ItemGroup>
</Project> </Project>

View File

@@ -8,6 +8,7 @@ namespace DysonNetwork.Develop.Identity;
public class CustomAppService( public class CustomAppService(
AppDatabase db, AppDatabase db,
FileReferenceService.FileReferenceServiceClient fileRefs,
FileService.FileServiceClient files FileService.FileServiceClient files
) )
{ {
@@ -46,8 +47,15 @@ public class CustomAppService(
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
if (request.Status == Shared.Models.CustomAppStatus.Production) // Create a new reference
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.PictureId }); await fileRefs.CreateReferenceAsync(
new CreateReferenceRequest
{
FileId = picture.Id,
Usage = "custom-apps.picture",
ResourceId = app.ResourceIdentifier
}
);
} }
if (request.BackgroundId is not null) if (request.BackgroundId is not null)
{ {
@@ -58,8 +66,15 @@ public class CustomAppService(
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
app.Background = SnCloudFileReferenceObject.FromProtoValue(background); app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
if (request.Status == Shared.Models.CustomAppStatus.Production) // Create a new reference
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.BackgroundId }); await fileRefs.CreateReferenceAsync(
new CreateReferenceRequest
{
FileId = background.Id,
Usage = "custom-apps.background",
ResourceId = app.ResourceIdentifier
}
);
} }
db.CustomApps.Add(app); db.CustomApps.Add(app);
@@ -170,7 +185,6 @@ public class CustomAppService(
public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request) public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request)
{ {
var oldStatus = app.Status;
if (request.Slug is not null) if (request.Slug is not null)
app.Slug = request.Slug; app.Slug = request.Slug;
if (request.Name is not null) if (request.Name is not null)
@@ -196,8 +210,15 @@ public class CustomAppService(
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
if (app.Status == Shared.Models.CustomAppStatus.Production) // Create a new reference
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.PictureId }); await fileRefs.CreateReferenceAsync(
new CreateReferenceRequest
{
FileId = picture.Id,
Usage = "custom-apps.picture",
ResourceId = app.ResourceIdentifier
}
);
} }
if (request.BackgroundId is not null) if (request.BackgroundId is not null)
{ {
@@ -208,28 +229,20 @@ public class CustomAppService(
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
app.Background = SnCloudFileReferenceObject.FromProtoValue(background); app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
if (app.Status == Shared.Models.CustomAppStatus.Production) // Create a new reference
await files.SetFilePublicAsync(new SetFilePublicRequest { FileId = request.BackgroundId }); await fileRefs.CreateReferenceAsync(
new CreateReferenceRequest
{
FileId = background.Id,
Usage = "custom-apps.background",
ResourceId = app.ResourceIdentifier
}
);
} }
db.Update(app); db.Update(app);
await db.SaveChangesAsync(); 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; return app;
} }
@@ -244,6 +257,12 @@ public class CustomAppService(
db.CustomApps.Remove(app); db.CustomApps.Remove(app);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
{
ResourceId = app.ResourceIdentifier
}
);
return true; return true;
} }
} }

View File

@@ -1,382 +0,0 @@
// <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
}
}
}

View File

@@ -1,53 +0,0 @@
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");
}
}
}

View File

@@ -19,12 +19,12 @@ namespace DysonNetwork.Develop.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.1") .HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Shared.Models.SnBotAccount", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -66,7 +66,7 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("bot_accounts", (string)null); b.ToTable("bot_accounts", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -139,7 +139,7 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("custom_apps", (string)null); b.ToTable("custom_apps", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -190,7 +190,24 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("custom_app_secrets", (string)null); b.ToTable("custom_app_secrets", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b => 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 =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -240,73 +257,9 @@ namespace DysonNetwork.Develop.Migrations
b.ToTable("dev_projects", (string)null); b.ToTable("dev_projects", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
{ {
b.Property<Guid>("Id") b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
.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() .WithMany()
.HasForeignKey("ProjectId") .HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -316,9 +269,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Project"); b.Navigation("Project");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomApp", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnDevProject", "Project") b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
.WithMany() .WithMany()
.HasForeignKey("ProjectId") .HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -328,9 +281,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Project"); b.Navigation("Project");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCustomAppSecret", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnCustomApp", "App") b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
.WithMany("Secrets") .WithMany("Secrets")
.HasForeignKey("AppId") .HasForeignKey("AppId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -340,9 +293,9 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("App"); b.Navigation("App");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDevProject", b => modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnDeveloper", "Developer") b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
.WithMany("Projects") .WithMany("Projects")
.HasForeignKey("DeveloperId") .HasForeignKey("DeveloperId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -352,24 +305,12 @@ namespace DysonNetwork.Develop.Migrations
b.Navigation("Developer"); b.Navigation("Developer");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMiniApp", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", 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"); b.Navigation("Secrets");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnDeveloper", b => modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
{ {
b.Navigation("Projects"); b.Navigation("Projects");
}); });

View File

@@ -1,185 +0,0 @@
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();
}
}

View File

@@ -1,22 +0,0 @@
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);
}
}

View File

@@ -1,92 +0,0 @@
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;
}
}

View File

@@ -1,22 +1,21 @@
using DysonNetwork.Develop; using DysonNetwork.Develop;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Develop.Startup; using DysonNetwork.Develop.Startup;
using DysonNetwork.Shared.Networking;
using DysonNetwork.Shared.Registry; using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); builder.AddServiceDefaults("develop");
builder.Services.Configure<ServiceRegistrationOptions>(opts => { opts.Name = "develop"; });
builder.ConfigureAppKestrel(builder.Configuration); builder.ConfigureAppKestrel(builder.Configuration);
builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication(); builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();
builder.Services.AddSphereService();
builder.Services.AddAccountService();
builder.Services.AddDriveService();
builder.AddSwaggerManifest( builder.AddSwaggerManifest(
"DysonNetwork.Develop", "DysonNetwork.Develop",

View File

@@ -8,7 +8,7 @@ namespace DysonNetwork.Develop.Project;
[ApiController] [ApiController]
[Route("/api/developers/{pubName}/projects")] [Route("/api/developers/{pubName}/projects")]
public class DevProjectController(DevProjectService ps, DeveloperService ds) : ControllerBase public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase
{ {
public record DevProjectRequest( public record DevProjectRequest(
[MaxLength(1024)] string? Slug, [MaxLength(1024)] string? Slug,
@@ -19,20 +19,20 @@ public class DevProjectController(DevProjectService ps, DeveloperService ds) : C
[HttpGet] [HttpGet]
public async Task<IActionResult> ListProjects([FromRoute] string pubName) public async Task<IActionResult> ListProjects([FromRoute] string pubName)
{ {
var developer = await ds.GetDeveloperByName(pubName); var developer = await developerService.GetDeveloperByName(pubName);
if (developer is null) return NotFound(); if (developer is null) return NotFound();
var projects = await ps.GetProjectsByDeveloperAsync(developer.Id); var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id);
return Ok(projects); return Ok(projects);
} }
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id) public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id)
{ {
var developer = await ds.GetDeveloperByName(pubName); var developer = await developerService.GetDeveloperByName(pubName);
if (developer is null) return NotFound(); if (developer is null) return NotFound();
var project = await ps.GetProjectAsync(id, developer.Id); var project = await projectService.GetProjectAsync(id, developer.Id);
if (project is null) return NotFound(); if (project is null) return NotFound();
return Ok(project); return Ok(project);
@@ -45,17 +45,17 @@ public class DevProjectController(DevProjectService ps, DeveloperService ds) : C
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized(); return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName); var developer = await developerService.GetDeveloperByName(pubName);
if (developer is null) if (developer is null)
return NotFound("Developer not found"); return NotFound("Developer not found");
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the developer to create a project"); return StatusCode(403, "You must be an editor of the developer to create a project");
if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name)) if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name))
return BadRequest("Slug and Name are required"); return BadRequest("Slug and Name are required");
var project = await ps.CreateProjectAsync(developer, request); var project = await projectService.CreateProjectAsync(developer, request);
return CreatedAtAction( return CreatedAtAction(
nameof(GetProject), nameof(GetProject),
new { pubName, id = project.Id }, new { pubName, id = project.Id },
@@ -74,15 +74,12 @@ public class DevProjectController(DevProjectService ps, DeveloperService ds) : C
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized(); return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName); var developer = await developerService.GetDeveloperByName(pubName);
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
if (developer is null || developer.Id != accountId)
if (developer is null)
return Forbid(); 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); var project = await projectService.UpdateProjectAsync(id, developer.Id, request);
if (project is null) if (project is null)
return NotFound(); return NotFound();
@@ -96,14 +93,12 @@ public class DevProjectController(DevProjectService ps, DeveloperService ds) : C
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized(); return Unauthorized();
var developer = await ds.GetDeveloperByName(pubName); var developer = await developerService.GetDeveloperByName(pubName);
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
if (developer is null) if (developer is null || developer.Id != accountId)
return Forbid(); 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 ps.DeleteProjectAsync(id, developer.Id); var success = await projectService.DeleteProjectAsync(id, developer.Id);
if (!success) if (!success)
return NotFound(); return NotFound();

View File

@@ -1,6 +1,6 @@
using DysonNetwork.Develop.Identity; using DysonNetwork.Develop.Identity;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Networking; using DysonNetwork.Shared.Http;
namespace DysonNetwork.Develop.Startup; namespace DysonNetwork.Develop.Startup;

View File

@@ -48,7 +48,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<CustomAppService>(); services.AddScoped<CustomAppService>();
services.AddScoped<DevProjectService>(); services.AddScoped<DevProjectService>();
services.AddScoped<BotAccountService>(); services.AddScoped<BotAccountService>();
services.AddScoped<MiniApp.MiniAppService>();
return services; return services;
} }

View File

@@ -10,7 +10,10 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" "App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"Registrar": "127.0.0.1:2379",
"Cache": "127.0.0.1:6379",
"Queue": "127.0.0.1:4222"
}, },
"KnownProxies": [ "KnownProxies": [
"127.0.0.1", "127.0.0.1",
@@ -20,6 +23,9 @@
"PublicBasePath": "/develop" "PublicBasePath": "/develop"
}, },
"Cache": { "Cache": {
"Serializer": "JSON" "Serializer": "MessagePack"
},
"Etcd": {
"Insecure": true
} }
} }

View File

@@ -23,12 +23,11 @@ public class AppDatabase(
public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
public DbSet<SnCloudFile> Files { get; set; } = null!; public DbSet<SnCloudFile> Files { get; set; } = null!;
public DbSet<SnFileObject> FileObjects { get; set; } = null!; public DbSet<SnCloudFileReference> FileReferences { 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<SnCloudFileIndex> FileIndexes { get; set; }
public DbSet<PersistentTask> Tasks { get; set; } = null!; public DbSet<PersistentTask> Tasks { get; set; } = null!;
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

View File

@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Drive.Billing; namespace DysonNetwork.Drive.Billing;
@@ -30,46 +29,28 @@ public class UsageService(AppDatabase db)
public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId) public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); 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 replicaData = await db.FileReplicas var poolUsages = await db.Pools
.Where(r => r.Status == SnFileReplicaStatus.Available) .Select(p => new UsageDetails
.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; PoolId = p.Id,
var pool = db.Pools.Local.FirstOrDefault(p => p.Id == poolId) PoolName = p.Name,
?? db.Pools.Find(poolId); UsageBytes = fileQuery
var multiplier = pool?.BillingConfig.CostMultiplier ?? 1.0; .Where(f => f.PoolId == p.Id)
var totalBytes = g.Sum(x => x.Size); .Sum(f => f.Size),
Cost = fileQuery
return new UsageDetails .Where(f => f.PoolId == p.Id)
{ .Sum(f => f.Size) / 1024.0 / 1024.0 *
PoolId = poolId, (p.BillingConfig.CostMultiplier ?? 1.0),
PoolName = pool?.Name ?? "Unknown", FileCount = fileQuery
UsageBytes = totalBytes, .Count(f => f.PoolId == p.Id)
Cost = totalBytes * multiplier / 1024.0 / 1024.0,
FileCount = g.Count()
};
}) })
.ToList(); .ToListAsync();
var totalUsage = poolUsages.Sum(p => p.UsageBytes); var totalUsage = poolUsages.Sum(p => p.UsageBytes);
var totalFileCount = poolUsages.Sum(p => p.FileCount); var totalFileCount = poolUsages.Sum(p => p.FileCount);
@@ -92,27 +73,17 @@ public class UsageService(AppDatabase db)
} }
var now = SystemClock.Instance.GetCurrentInstant(); 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 replicaData = await db.FileReplicas var usageBytes = await fileQuery
.Where(r => r.PoolId == poolId) .SumAsync(f => f.Size);
.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 = replicaData.Count; var fileCount = await fileQuery
.CountAsync();
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 * var cost = usageBytes / 1024.0 / 1024.0 *
(pool.BillingConfig.CostMultiplier ?? 1.0); (pool.BillingConfig.CostMultiplier ?? 1.0);
@@ -130,23 +101,20 @@ public class UsageService(AppDatabase db)
public async Task<long> GetTotalBillableUsage(Guid accountId) public async Task<long> GetTotalBillableUsage(Guid accountId)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var files = await db.Files
var billingData = await (from f in db.Files .Where(f => f.AccountId == accountId)
where f.AccountId == accountId .Where(f => f.PoolId.HasValue)
where !f.IsMarkedRecycle .Where(f => !f.IsMarkedRecycle)
where !f.ExpiredAt.HasValue || f.ExpiredAt > now .Include(f => f.Pool)
from r in f.Object!.FileReplicas .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now)
where r.Status == SnFileReplicaStatus.Available .Select(f => new
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, f.Size,
Multiplier = p.BillingConfig.CostMultiplier ?? 1.0 Multiplier = f.Pool!.BillingConfig.CostMultiplier ?? 1.0
}).ToListAsync(); })
.ToListAsync();
var totalCost = billingData.Sum(x => x.Size * x.Multiplier) / 1024.0 / 1024.0; var totalCost = files.Sum(f => f.Size * f.Multiplier) / 1024.0 / 1024.0;
return (long)Math.Ceiling(totalCost); return (long)Math.Ceiling(totalCost);
} }

View File

@@ -11,9 +11,9 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageReference Include="FFMpegCore" Version="5.4.0" /> <PackageReference Include="FFMpegCore" Version="5.4.0" />
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" /> <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -27,13 +27,14 @@
<PackageReference Include="NetVips" Version="3.1.0" /> <PackageReference Include="NetVips" Version="3.1.0" />
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.3" /> <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.3" />
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.3" /> <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.3" />
<PackageReference Include="NodaTime" Version="3.2.3" /> <PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Quartz" Version="3.15.1" /> <PackageReference Include="Quartz" Version="3.15.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" /> <PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
<PackageReference Include="Quartz.Extensions.Hosting" 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 --> <!-- 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" Version="2.88.9" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />

View File

@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Drive.Storage; using DysonNetwork.Drive.Storage;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Networking;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -56,8 +56,8 @@ public class FileIndexController(
{ {
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList() "name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(), : fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Object!.Size).ToList() "size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
: fileIndexes.OrderBy(fi => fi.File.Object!.Size).ToList(), : fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList() _ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList() : fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
}; };
@@ -211,31 +211,31 @@ public class FileIndexController(
try try
{ {
var baseQuery = db.Files var filesQuery = db.Files
.Where(f => f.AccountId == accountId .Where(f => f.AccountId == accountId
&& f.IsMarkedRecycle == recycled && f.IsMarkedRecycle == recycled
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId) && !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)
) )
.Include(f => f.Object)
.AsQueryable(); .AsQueryable();
if (pool.HasValue) baseQuery = baseQuery.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId == pool.Value)); // 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 (!string.IsNullOrWhiteSpace(query)) if (!string.IsNullOrWhiteSpace(query))
{ {
baseQuery = baseQuery.Where(f => f.Name.Contains(query)); filesQuery = filesQuery.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(); var totalCount = await filesQuery.CountAsync();
Response.Headers.Append("X-Total", totalCount.ToString()); Response.Headers.Append("X-Total", totalCount.ToString());
@@ -545,7 +545,6 @@ public class FileIndexController(
var fileIndexes = await db.FileIndexes var fileIndexes = await db.FileIndexes
.Where(fi => fi.AccountId == accountId) .Where(fi => fi.AccountId == accountId)
.Include(fi => fi.File) .Include(fi => fi.File)
.ThenInclude(f => f.Object)
.Where(fi => .Where(fi =>
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) && (string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
(fi.File.Name.ToLower().Contains(searchTerm) || (fi.File.Name.ToLower().Contains(searchTerm) ||

View File

@@ -141,7 +141,6 @@ public class FileIndexService(AppDatabase db)
return await db.FileIndexes return await db.FileIndexes
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath) .Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
.Include(fi => fi.File) .Include(fi => fi.File)
.ThenInclude(f => f.Object)
.ToListAsync(); .ToListAsync();
} }
@@ -155,7 +154,6 @@ public class FileIndexService(AppDatabase db)
return await db.FileIndexes return await db.FileIndexes
.Where(fi => fi.FileId == fileId) .Where(fi => fi.FileId == fileId)
.Include(fi => fi.File) .Include(fi => fi.File)
.ThenInclude(f => f.Object)
.ToListAsync(); .ToListAsync();
} }
@@ -169,7 +167,6 @@ public class FileIndexService(AppDatabase db)
return await db.FileIndexes return await db.FileIndexes
.Where(fi => fi.AccountId == accountId) .Where(fi => fi.AccountId == accountId)
.Include(fi => fi.File) .Include(fi => fi.File)
.ThenInclude(f => f.Object)
.ToListAsync(); .ToListAsync();
} }

View File

@@ -1,560 +0,0 @@
// <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
}
}
}

View File

@@ -1,155 +0,0 @@
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);
}
}
}

View File

@@ -1,632 +0,0 @@
// <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
}
}
}

View File

@@ -1,155 +0,0 @@
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");
}
}
}

View File

@@ -1,762 +0,0 @@
// <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
}
}
}

View File

@@ -1,173 +0,0 @@
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");
}
}
}

View File

@@ -1,760 +0,0 @@
// <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
}
}
}

View File

@@ -1,60 +0,0 @@
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);
}
}
}

View File

@@ -1,688 +0,0 @@
// <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
}
}
}

View File

@@ -1,155 +0,0 @@
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);
}
}
}

View File

@@ -1,658 +0,0 @@
// <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
}
}
}

View File

@@ -1,95 +0,0 @@
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);
}
}
}

View File

@@ -1,654 +0,0 @@
// <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
}
}
}

View File

@@ -1,30 +0,0 @@
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"));
}
}
}

View File

@@ -1,640 +0,0 @@
// <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
}
}
}

View File

@@ -1,49 +0,0 @@
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");
}
}
}

View File

@@ -20,7 +20,7 @@ namespace DysonNetwork.Drive.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.1") .HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -100,6 +100,12 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("description"); .HasColumnName("description");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)")
.HasColumnName("discriminator");
b.Property<string>("ErrorMessage") b.Property<string>("ErrorMessage")
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
@@ -167,6 +173,60 @@ namespace DysonNetwork.Drive.Migrations
.HasName("pk_tasks"); .HasName("pk_tasks");
b.ToTable("tasks", (string)null); 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 => modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
@@ -261,25 +321,54 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .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") b.Property<bool>("IsMarkedRecycle")
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("is_marked_recycle"); .HasColumnName("is_marked_recycle");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("name"); .HasColumnName("name");
b.Property<string>("ObjectId") b.Property<Guid?>("PoolId")
.HasMaxLength(32) .HasColumnType("uuid")
.HasColumnType("character varying(32)") .HasColumnName("pool_id");
.HasColumnName("object_id");
b.PrimitiveCollection<string>("SensitiveMarks") b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("sensitive_marks"); .HasColumnName("sensitive_marks");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<string>("StorageId") b.Property<string>("StorageId")
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")
@@ -308,8 +397,8 @@ namespace DysonNetwork.Drive.Migrations
b.HasIndex("BundleId") b.HasIndex("BundleId")
.HasDatabaseName("ix_files_bundle_id"); .HasDatabaseName("ix_files_bundle_id");
b.HasIndex("ObjectId") b.HasIndex("PoolId")
.HasDatabaseName("ix_files_object_id"); .HasDatabaseName("ix_files_pool_id");
b.ToTable("files", (string)null); b.ToTable("files", (string)null);
}); });
@@ -420,153 +509,78 @@ namespace DysonNetwork.Drive.Migrations
b.ToTable("bundles", (string)null); b.ToTable("bundles", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b => modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
{ {
b.Property<string>("Id") b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<Instant>("CreatedAt") b.Property<Guid?>("BundleId")
.HasColumnType("timestamp with time zone") .HasColumnType("uuid")
.HasColumnName("created_at"); .HasColumnName("bundle_id");
b.Property<Instant?>("DeletedAt") b.Property<long>("ChunkSize")
.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") .HasColumnType("bigint")
.HasColumnName("size"); .HasColumnName("chunk_size");
b.Property<Instant>("UpdatedAt") b.Property<int>("ChunksCount")
.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") .HasColumnType("integer")
.HasColumnName("permission"); .HasColumnName("chunks_count");
b.Property<string>("SubjectId") b.Property<int>("ChunksUploaded")
.IsRequired()
.HasColumnType("text")
.HasColumnName("subject_id");
b.Property<int>("SubjectType")
.HasColumnType("integer") .HasColumnType("integer")
.HasColumnName("subject_type"); .HasColumnName("chunks_uploaded");
b.Property<Instant>("UpdatedAt") b.Property<string>("ContentType")
.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() .IsRequired()
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("character varying(128)") .HasColumnType("character varying(128)")
.HasColumnName("storage_id"); .HasColumnName("content_type");
b.Property<Instant>("UpdatedAt") b.Property<string>("EncryptPassword")
.HasColumnType("timestamp with time zone") .HasMaxLength(256)
.HasColumnName("updated_at"); .HasColumnType("character varying(256)")
.HasColumnName("encrypt_password");
b.HasKey("Id") b.Property<string>("FileName")
.HasName("pk_file_replicas"); .IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("file_name");
b.HasIndex("ObjectId") b.Property<long>("FileSize")
.HasDatabaseName("ix_file_replicas_object_id"); .HasColumnType("bigint")
.HasColumnName("file_size");
b.HasIndex("PoolId") b.Property<string>("Hash")
.HasDatabaseName("ix_file_replicas_pool_id"); .IsRequired()
.HasColumnType("text")
.HasColumnName("hash");
b.ToTable("file_replicas", (string)null); 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.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 => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
@@ -576,14 +590,14 @@ namespace DysonNetwork.Drive.Migrations
.HasForeignKey("BundleId") .HasForeignKey("BundleId")
.HasConstraintName("fk_files_bundles_bundle_id"); .HasConstraintName("fk_files_bundles_bundle_id");
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object") b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
.WithMany() .WithMany()
.HasForeignKey("ObjectId") .HasForeignKey("PoolId")
.HasConstraintName("fk_files_file_objects_object_id"); .HasConstraintName("fk_files_pools_pool_id");
b.Navigation("Bundle"); b.Navigation("Bundle");
b.Navigation("Object"); b.Navigation("Pool");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
@@ -598,39 +612,17 @@ namespace DysonNetwork.Drive.Migrations
b.Navigation("File"); 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 => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
{ {
b.Navigation("FileIndexes"); b.Navigation("FileIndexes");
b.Navigation("References");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
{ {
b.Navigation("Files"); b.Navigation("Files");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
{
b.Navigation("FileReplicas");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@@ -1,13 +1,15 @@
using DysonNetwork.Drive; using DysonNetwork.Drive;
using DysonNetwork.Drive.Startup; using DysonNetwork.Drive.Startup;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Networking; using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Registry; using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); builder.AddServiceDefaults("drive");
builder.Services.Configure<ServiceRegistrationOptions>(opts => { opts.Name = "drive"; });
// Configure Kestrel and server options // Configure Kestrel and server options
builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue);
@@ -17,11 +19,9 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV
builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication(); builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();
builder.Services.AddRingService();
builder.Services.AddAccountService();
builder.Services.AddAppFlushHandlers(); builder.Services.AddAppFlushHandlers();
builder.Services.AddAppBusinessServices(builder.Configuration); builder.Services.AddAppBusinessServices();
builder.Services.AddAppScheduledJobs(); builder.Services.AddAppScheduledJobs();
builder.AddSwaggerManifest( builder.AddSwaggerManifest(

View File

@@ -16,6 +16,7 @@ public static class ApplicationBuilderExtensions
{ {
// Map your gRPC services here // Map your gRPC services here
app.MapGrpcService<FileServiceGrpc>(); app.MapGrpcService<FileServiceGrpc>();
app.MapGrpcService<FileReferenceServiceGrpc>();
app.MapGrpcReflectionService(); app.MapGrpcReflectionService();
return app; return app;

View File

@@ -3,7 +3,7 @@ using DysonNetwork.Drive.Storage;
using DysonNetwork.Drive.Storage.Model; using DysonNetwork.Drive.Storage.Model;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Queue; using DysonNetwork.Shared.Stream;
using FFMpegCore; using FFMpegCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NATS.Client.Core; using NATS.Client.Core;
@@ -156,45 +156,18 @@ public class BroadcastEventHandler(
logger.LogInformation("Processing file {FileId} in background...", fileId); logger.LogInformation("Processing file {FileId} in background...", fileId);
var fileToUpdate = await scopedDb.Files var fileToUpdate = await scopedDb.Files.AsNoTracking().FirstAsync(f => f.Id == fileId);
.AsNoTracking()
.Include(f => f.Object)
.FirstAsync(f => f.Id == fileId);
// Find the upload task associated with this file // Find the upload task associated with this file
var baseTask = await scopedDb.Tasks var uploadTask = await scopedDb.Tasks
.Where(t => t.Type == TaskType.FileUpload) .OfType<PersistentUploadTask>()
.FirstOrDefaultAsync(); .FirstOrDefaultAsync(t => t.FileName == fileToUpdate.Name && t.FileSize == fileToUpdate.Size);
var uploadTask = baseTask != null ? new PersistentUploadTask if (fileToUpdate.IsEncrypted)
{ {
Id = baseTask.Id, uploads.Add((processingFilePath, string.Empty, contentType, false));
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); var fileExtension = Path.GetExtension(processingFilePath);
switch (contentType.Split('/')[0]) switch (contentType.Split('/')[0])
@@ -314,26 +287,12 @@ public class BroadcastEventHandler(
logger.LogInformation("Uploaded file {FileId} done!", fileId); logger.LogInformation("Uploaded file {FileId} done!", fileId);
var now = SystemClock.Instance.GetCurrentInstant(); 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 await scopedDb.Files.Where(f => f.Id == fileId).ExecuteUpdateAsync(setter => setter
.SetProperty(f => f.UploadedAt, now) .SetProperty(f => f.UploadedAt, now)
); .SetProperty(f => f.PoolId, destPool)
.SetProperty(f => f.MimeType, newMimeType)
await scopedDb.FileObjects.Where(fo => fo.Id == fileId).ExecuteUpdateAsync(setter => setter .SetProperty(f => f.HasCompression, hasCompression)
.SetProperty(fo => fo.MimeType, newMimeType) .SetProperty(f => f.HasThumbnail, hasThumbnail)
.SetProperty(fo => fo.HasCompression, hasCompression)
.SetProperty(fo => fo.HasThumbnail, hasThumbnail)
); );
// Only delete temp file after successful upload and db update // Only delete temp file after successful upload and db update

View File

@@ -29,13 +29,6 @@ public static class ScheduledJobsConfiguration
.ForJob(persistentTaskCleanupJob) .ForJob(persistentTaskCleanupJob)
.WithIdentity("PersistentTaskCleanupTrigger") .WithIdentity("PersistentTaskCleanupTrigger")
.WithCronSchedule("0 0 2 * * ?")); // Run daily at 2 AM .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); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

View File

@@ -9,64 +9,58 @@ namespace DysonNetwork.Drive.Startup;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
extension(IServiceCollection services) public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
{ {
public IServiceCollection AddAppServices(IConfiguration configuration) services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase
services.AddHttpContextAccessor();
services.AddHttpClient();
// Register gRPC services
services.AddGrpc(options =>
{ {
services.AddDbContext<AppDatabase>(); options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
services.AddHttpContextAccessor(); options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
});
services.AddGrpcReflection();
services.AddHttpClient(); 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();
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()
{ {
services.AddAuthorization(); options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
return services; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
} options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
public IServiceCollection AddAppFlushHandlers() options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
{ });
services.AddSingleton<FlushBufferService>();
return services; return services;
} }
public IServiceCollection AddAppBusinessServices(IConfiguration configuration) public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
{ {
services.Configure<Storage.Options.FileReanalysisOptions>(configuration.GetSection("FileReanalysis")); services.AddAuthorization();
return services;
}
services.AddScoped<Storage.FileService>(); public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
services.AddScoped<Storage.FileReanalysisService>(); {
services.AddScoped<Storage.PersistentTaskService>(); services.AddSingleton<FlushBufferService>();
services.AddScoped<FileIndexService>();
services.AddScoped<Billing.UsageService>();
services.AddScoped<Billing.QuotaService>();
services.AddHostedService<BroadcastEventHandler>(); return services;
services.AddHostedService<Storage.FileReanalysisBackgroundService>(); }
return services; 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;
} }
} }

View File

@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using DysonNetwork.Shared.Models;
using Quartz; using Quartz;
namespace DysonNetwork.Drive.Storage; namespace DysonNetwork.Drive.Storage;
@@ -41,16 +40,14 @@ public class CloudFileUnusedRecyclingJob(
var markedCount = 0; var markedCount = 0;
var totalFiles = await db.Files var totalFiles = await db.Files
.Where(f => f.FileIndexes.Count == 0) .Where(f => f.FileIndexes.Count == 0)
.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId.HasValue && recyclablePools.Contains(r.PoolId.Value))) .Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value))
.Where(f => !f.IsMarkedRecycle) .Where(f => !f.IsMarkedRecycle)
.Include(f => f.Object)
.ThenInclude(o => o.FileReplicas)
.CountAsync(); .CountAsync();
logger.LogInformation("Found {TotalFiles} files to check for unused status", totalFiles); 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 // Define a timestamp to limit the age of files we're processing in this run
// This spreads processing across multiple job runs for very large databases // This spreads the 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 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 // Instead of loading all files at once, use pagination
@@ -59,18 +56,17 @@ public class CloudFileUnusedRecyclingJob(
while (hasMoreFiles) while (hasMoreFiles)
{ {
IQueryable<SnCloudFile> baseQuery = db.Files // Query for the next batch of files using keyset pagination
.Where(f => f.Object!.FileReplicas.Any(r => r.PoolId.HasValue && recyclablePools.Contains(r.PoolId.Value))) var filesQuery = db.Files
.Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value))
.Where(f => !f.IsMarkedRecycle) .Where(f => !f.IsMarkedRecycle)
.Where(f => f.CreatedAt <= ageThreshold) .Where(f => f.CreatedAt <= ageThreshold); // Only process older files first
.Include(f => f.Object)
.ThenInclude(o => o.FileReplicas);
if (lastProcessedId != null) if (lastProcessedId != null)
baseQuery = baseQuery.Where(f => string.Compare(f.Id, lastProcessedId) > 0); filesQuery = filesQuery.Where(f => string.Compare(f.Id, lastProcessedId) > 0);
var fileBatch = await baseQuery var fileBatch = await filesQuery
.OrderBy(f => f.Id) .OrderBy(f => f.Id) // Ensure consistent ordering for pagination
.Take(batchSize) .Take(batchSize)
.Select(f => f.Id) .Select(f => f.Id)
.ToListAsync(); .ToListAsync();
@@ -84,11 +80,13 @@ public class CloudFileUnusedRecyclingJob(
processedCount += fileBatch.Count; processedCount += fileBatch.Count;
lastProcessedId = fileBatch.Last(); lastProcessedId = fileBatch.Last();
// Optimized query: Find files that have no file object or no replicas // Optimized query: Find files that have no references OR all references are expired
// A file is considered "unused" if its file object has no replicas // This replaces the memory-intensive approach of loading all references
var filesToMark = await db.Files var filesToMark = await db.Files
.Where(f => fileBatch.Contains(f.Id)) .Where(f => fileBatch.Contains(f.Id))
.Where(f => f.Object == null || f.Object.FileReplicas.Count == 0) .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)))
.Select(f => f.Id) .Select(f => f.Id)
.ToListAsync(); .ToListAsync();

View File

@@ -1,5 +1,3 @@
using System.Security.Cryptography;
using System.Text;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
@@ -16,14 +14,10 @@ public class FileController(
AppDatabase db, AppDatabase db,
FileService fs, FileService fs,
IConfiguration configuration, IConfiguration configuration,
IWebHostEnvironment env IWebHostEnvironment env,
FileReferenceService fileReferenceService
) : ControllerBase ) : 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}")] [HttpGet("{id}")]
public async Task<ActionResult> OpenFile( public async Task<ActionResult> OpenFile(
string id, string id,
@@ -38,8 +32,7 @@ public class FileController(
var file = await fs.GetFileAsync(fileId); var file = await fs.GetFileAsync(fileId);
if (file is null) return NotFound("File not found."); if (file is null) return NotFound("File not found.");
var currentUser = HttpContext.Items["CurrentUser"] as Account; var accessResult = await ValidateFileAccess(file, passcode);
var accessResult = await ValidateFileAccess(file, passcode, currentUser);
if (accessResult is not null) return accessResult; if (accessResult is not null) return accessResult;
// Handle direct storage URL redirect // Handle direct storage URL redirect
@@ -54,7 +47,7 @@ public class FileController(
return await ServeRemoteFile(file, fileExtension, download, original, thumbnail, overrideMimeType); return await ServeRemoteFile(file, fileExtension, download, original, thumbnail, overrideMimeType);
} }
private static (string fileId, string? extension) ParseFileId(string id) private (string fileId, string? extension) ParseFileId(string id)
{ {
if (!id.Contains('.')) return (id, null); if (!id.Contains('.')) return (id, null);
@@ -62,186 +55,38 @@ public class FileController(
return (parts.First(), parts.Last()); 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)) if (file.Bundle is not null && !file.Bundle.VerifyPasscode(passcode))
return StatusCode(StatusCodes.Status403Forbidden, "The passcode is incorrect."); 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) private Task<ActionResult> ServeLocalFile(SnCloudFile file)
{ {
var currentUser = HttpContext.Items["CurrentUser"] as Account; // Try temp storage first
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); var tempFilePath = Path.Combine(Path.GetTempPath(), file.Id);
if (System.IO.File.Exists(tempFilePath)) if (System.IO.File.Exists(tempFilePath))
{ {
return PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream", if (file.IsEncrypted)
file.Name, enableRangeProcessing: true); 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));
} }
// Fallback for tus uploads
var tusStorePath = configuration.GetValue<string>("Storage:Uploads"); var tusStorePath = configuration.GetValue<string>("Storage:Uploads");
if (string.IsNullOrEmpty(tusStorePath)) if (string.IsNullOrEmpty(tusStorePath))
return StatusCode(StatusCodes.Status400BadRequest, return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status400BadRequest,
"File is being processed. Please try again later."); "File is being processed. Please try again later."));
var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id); var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
if (System.IO.File.Exists(tusFilePath)) return System.IO.File.Exists(tusFilePath)
{ ? Task.FromResult<ActionResult>(PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream",
return PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream", file.Name, enableRangeProcessing: true))
file.Name, enableRangeProcessing: true); : 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.");
}
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( private async Task<ActionResult> ServeRemoteFile(
@@ -253,12 +98,11 @@ public class FileController(
string? overrideMimeType string? overrideMimeType
) )
{ {
var primaryReplica = file.Object?.FileReplicas.FirstOrDefault(r => r.IsPrimary); if (!file.PoolId.HasValue)
if (primaryReplica == null || primaryReplica.PoolId == null)
return StatusCode(StatusCodes.Status500InternalServerError, return StatusCode(StatusCodes.Status500InternalServerError,
"File is in an inconsistent state: uploaded but no pool ID."); "File is in an inconsistent state: uploaded but no pool ID.");
var pool = await fs.GetPoolAsync(primaryReplica.PoolId.Value); var pool = await fs.GetPoolAsync(file.PoolId.Value);
if (pool is null) if (pool is null)
return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible."); return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible.");
@@ -301,7 +145,9 @@ public class FileController(
private ActionResult? TryProxyRedirect(SnCloudFile file, RemoteStorageConfig dest, string fileName) private ActionResult? TryProxyRedirect(SnCloudFile file, RemoteStorageConfig dest, string fileName)
{ {
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false)) if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
{
return Redirect(BuildProxyUrl(dest.ImageProxy, fileName)); return Redirect(BuildProxyUrl(dest.ImageProxy, fileName));
}
return dest.AccessProxy is not null ? Redirect(BuildProxyUrl(dest.AccessProxy, fileName)) : null; return dest.AccessProxy is not null ? Redirect(BuildProxyUrl(dest.AccessProxy, fileName)) : null;
} }
@@ -322,7 +168,7 @@ public class FileController(
string? overrideMimeType string? overrideMimeType
) )
{ {
var client = FileService.CreateMinioClient(dest); var client = fs.CreateMinioClient(dest);
if (client is null) if (client is null)
return BadRequest("Failed to configure client for remote destination, file got an invalid storage remote."); return BadRequest("Failed to configure client for remote destination, file got an invalid storage remote.");
@@ -336,9 +182,6 @@ public class FileController(
.WithHeaders(headers) .WithHeaders(headers)
); );
if (dest.AccessEndpoint is not null)
openUrl = openUrl.Replace($"{dest.Endpoint}/{dest.Bucket}", dest.AccessEndpoint);
return Redirect(openUrl); return Redirect(openUrl);
} }
@@ -388,18 +231,17 @@ public class FileController(
} }
[HttpGet("{id}/references")] [HttpGet("{id}/references")]
public async Task<ActionResult<List<SnCloudFile>>> GetFileReferences(string id) public async Task<ActionResult<List<Shared.Models.SnCloudFileReference>>> GetFileReferences(string id)
{ {
var file = await fs.GetFileAsync(id); var file = await fs.GetFileAsync(id);
if (file is null) return NotFound("File not found."); if (file is null) return NotFound("File not found.");
var currentUser = HttpContext.Items["CurrentUser"] as Account; // Check if user has access to the file
var accessResult = await ValidateFileAccess(file, null, currentUser); var accessResult = await ValidateFileAccess(file, null);
if (accessResult is not null) return accessResult; if (accessResult is not null) return accessResult;
var references = await db.Files // Get references using the injected FileReferenceService
.Where(f => f.ObjectId == file.ObjectId && f.Id != file.Id) var references = await fileReferenceService.GetReferencesAsync(id);
.ToListAsync();
return Ok(references); return Ok(references);
} }
@@ -462,10 +304,10 @@ public class FileController(
var filesQuery = db.Files var filesQuery = db.Files
.Where(e => e.IsMarkedRecycle == recycled) .Where(e => e.IsMarkedRecycle == recycled)
.Where(e => e.AccountId == accountId) .Where(e => e.AccountId == accountId)
.Include(e => e.Object) .Include(e => e.Pool)
.AsQueryable(); .AsQueryable();
if (pool.HasValue) filesQuery = filesQuery.Where(e => e.Object!.FileReplicas.Any(r => r.PoolId == pool.Value)); if (pool.HasValue) filesQuery = filesQuery.Where(e => e.PoolId == pool);
if (!string.IsNullOrWhiteSpace(query)) if (!string.IsNullOrWhiteSpace(query))
{ {

View File

@@ -0,0 +1,70 @@
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");
}
}

View File

@@ -1,97 +0,0 @@
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");
}
}

View File

@@ -1,27 +0,0 @@
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");
}
}

View File

@@ -1,577 +0,0 @@
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();
}
}

View File

@@ -0,0 +1,524 @@
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);
}
}

View File

@@ -0,0 +1,174 @@
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 };
}
}

View File

@@ -30,7 +30,7 @@ public class FileService(
public async Task<SnCloudFile?> GetFileAsync(string fileId) public async Task<SnCloudFile?> GetFileAsync(string fileId)
{ {
var cacheKey = string.Concat(CacheKeyPrefix, fileId); var cacheKey = $"{CacheKeyPrefix}{fileId}";
var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey);
if (cachedFile is not null) if (cachedFile is not null)
@@ -38,9 +38,8 @@ public class FileService(
var file = await db.Files var file = await db.Files
.Where(f => f.Id == fileId) .Where(f => f.Id == fileId)
.Include(f => f.Pool)
.Include(f => f.Bundle) .Include(f => f.Bundle)
.Include(f => f.Object)
.ThenInclude(o => o.FileReplicas)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (file != null) if (file != null)
@@ -56,7 +55,7 @@ public class FileService(
foreach (var fileId in fileIds) foreach (var fileId in fileIds)
{ {
var cacheKey = string.Concat(CacheKeyPrefix, fileId); var cacheKey = $"{CacheKeyPrefix}{fileId}";
var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey); var cachedFile = await cache.GetAsync<SnCloudFile>(cacheKey);
if (cachedFile != null) if (cachedFile != null)
@@ -69,14 +68,12 @@ public class FileService(
{ {
var dbFiles = await db.Files var dbFiles = await db.Files
.Where(f => uncachedIds.Contains(f.Id)) .Where(f => uncachedIds.Contains(f.Id))
.Include(f => f.Bundle) .Include(f => f.Pool)
.Include(f => f.Object)
.ThenInclude(o => o.FileReplicas)
.ToListAsync(); .ToListAsync();
foreach (var file in dbFiles) foreach (var file in dbFiles)
{ {
var cacheKey = string.Concat(CacheKeyPrefix, file.Id); var cacheKey = $"{CacheKeyPrefix}{file.Id}";
await cache.SetAsync(cacheKey, file, CacheDuration); await cache.SetAsync(cacheKey, file, CacheDuration);
cachedFiles[file.Id] = file; cachedFiles[file.Id] = file;
} }
@@ -109,9 +106,7 @@ public class FileService(
var (managedTempPath, fileSize, finalContentType) = var (managedTempPath, fileSize, finalContentType) =
await PrepareFileAsync(fileId, filePath, fileName, contentType); await PrepareFileAsync(fileId, filePath, fileName, contentType);
var fileObject = CreateFileObject(fileId, accountId, finalContentType, fileSize); var file = CreateFileObject(fileId, fileName, finalContentType, fileSize, finalExpiredAt, bundle, accountId);
var file = CreateCloudFile(fileId, fileName, fileObject, finalExpiredAt, bundle, accountId);
if (!pool.PolicyConfig.NoMetadata) if (!pool.PolicyConfig.NoMetadata)
{ {
@@ -119,11 +114,11 @@ public class FileService(
} }
var (processingPath, isTempFile) = var (processingPath, isTempFile) =
await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, fileObject); await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, file);
fileObject.Hash = await HashFileAsync(processingPath); file.Hash = await HashFileAsync(processingPath);
await SaveFileToDatabaseAsync(file, fileObject, pool.Id); await SaveFileToDatabaseAsync(file);
await PublishFileUploadedEventAsync(file, pool, processingPath, isTempFile); await PublishFileUploadedEventAsync(file, pool, processingPath, isTempFile);
@@ -183,25 +178,11 @@ public class FileService(
return (managedTempPath, fileSize, finalContentType); return (managedTempPath, fileSize, finalContentType);
} }
private SnFileObject CreateFileObject( private SnCloudFile CreateFileObject(
string fileId,
Guid accountId,
string contentType,
long fileSize
)
{
return new SnFileObject
{
Id = fileId,
MimeType = contentType,
Size = fileSize,
};
}
private SnCloudFile CreateCloudFile(
string fileId, string fileId,
string fileName, string fileName,
SnFileObject fileObject, string contentType,
long fileSize,
Instant? expiredAt, Instant? expiredAt,
SnFileBundle? bundle, SnFileBundle? bundle,
Guid accountId Guid accountId
@@ -211,24 +192,24 @@ public class FileService(
{ {
Id = fileId, Id = fileId,
Name = fileName, Name = fileName,
Object = fileObject, MimeType = contentType,
ObjectId = fileId, Size = fileSize,
ExpiredAt = expiredAt, ExpiredAt = expiredAt,
BundleId = bundle?.Id, BundleId = bundle?.Id,
AccountId = accountId, AccountId = accountId,
}; };
} }
private Task<(string processingPath, bool isTempFile)> ProcessEncryptionAsync( private async Task<(string processingPath, bool isTempFile)> ProcessEncryptionAsync(
string fileId, string fileId,
string managedTempPath, string managedTempPath,
string? encryptPassword, string? encryptPassword,
FilePool pool, FilePool pool,
SnFileObject fileObject SnCloudFile file
) )
{ {
if (string.IsNullOrWhiteSpace(encryptPassword)) if (string.IsNullOrWhiteSpace(encryptPassword))
return Task.FromResult((managedTempPath, true)); return (managedTempPath, true);
if (!pool.PolicyConfig.AllowEncryption) if (!pool.PolicyConfig.AllowEncryption)
throw new InvalidOperationException("Encryption is not allowed in this pool"); throw new InvalidOperationException("Encryption is not allowed in this pool");
@@ -238,31 +219,17 @@ public class FileService(
File.Delete(managedTempPath); File.Delete(managedTempPath);
fileObject.MimeType = "application/octet-stream"; file.IsEncrypted = true;
fileObject.Size = new FileInfo(encryptedPath).Length; file.MimeType = "application/octet-stream";
file.Size = new FileInfo(encryptedPath).Length;
return Task.FromResult((encryptedPath, true)); return (encryptedPath, true);
} }
private async Task SaveFileToDatabaseAsync(SnCloudFile file, SnFileObject fileObject, Guid poolId) private async Task SaveFileToDatabaseAsync(SnCloudFile file)
{ {
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.Files.Add(file);
db.FileObjects.Add(fileObject);
db.FileReplicas.Add(replica);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
file.ObjectId = file.Id;
file.StorageId ??= file.Id; file.StorageId ??= file.Id;
} }
@@ -285,8 +252,6 @@ public class FileService(
private async Task ExtractMetadataAsync(SnCloudFile file, string filePath) private async Task ExtractMetadataAsync(SnCloudFile file, string filePath)
{ {
if (file.Object == null) return;
switch (file.MimeType?.Split('/')[0]) switch (file.MimeType?.Split('/')[0])
{ {
case "image": case "image":
@@ -332,11 +297,11 @@ public class FileService(
if (orientation is 6 or 8) (width, height) = (height, width); if (orientation is 6 or 8) (width, height) = (height, width);
meta["exif"] = exif; meta["exif"] = exif;
meta["ratio"] = height != 0 ? (double)width / height : 0; meta["ratio"] = height != 0 ? (double)width / height : 0;
file.Object.Meta = meta; file.FileMeta = meta;
} }
catch (Exception ex) catch (Exception ex)
{ {
file.Object.Meta = new Dictionary<string, object?>(); file.FileMeta = new Dictionary<string, object?>();
logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id); logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id);
} }
@@ -347,7 +312,7 @@ public class FileService(
try try
{ {
var mediaInfo = await FFProbe.AnalyseAsync(filePath); var mediaInfo = await FFProbe.AnalyseAsync(filePath);
file.Object.Meta = new Dictionary<string, object?> file.FileMeta = new Dictionary<string, object?>
{ {
["width"] = mediaInfo.PrimaryVideoStream?.Width, ["width"] = mediaInfo.PrimaryVideoStream?.Width,
["height"] = mediaInfo.PrimaryVideoStream?.Height, ["height"] = mediaInfo.PrimaryVideoStream?.Height,
@@ -383,8 +348,8 @@ public class FileService(
.ToList(), .ToList(),
}; };
if (mediaInfo.PrimaryVideoStream is not null) if (mediaInfo.PrimaryVideoStream is not null)
file.Object.Meta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width / file.FileMeta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width /
mediaInfo.PrimaryVideoStream.Height; mediaInfo.PrimaryVideoStream.Height;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -505,20 +470,8 @@ public class FileService(
await db.Files.Where(f => f.Id == file.Id).ExecuteUpdateAsync(updatable.ToSetPropertyCalls()); 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); await _PurgeCacheAsync(file.Id);
return await db.Files return await db.Files.AsNoTracking().FirstAsync(f => f.Id == file.Id);
.AsNoTracking()
.Include(f => f.Object)
.ThenInclude(o => o.FileReplicas)
.FirstAsync(f => f.Id == file.Id);
} }
public async Task DeleteFileAsync(SnCloudFile file, bool skipData = false) public async Task DeleteFileAsync(SnCloudFile file, bool skipData = false)
@@ -528,46 +481,17 @@ public class FileService(
await _PurgeCacheAsync(file.Id); await _PurgeCacheAsync(file.Id);
if (!skipData) 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) public async Task DeleteFileDataAsync(SnCloudFile file, bool force = false)
{ {
if (file.ObjectId == null) return; if (!file.PoolId.HasValue) 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) if (!force)
{ {
var sameOriginFiles = await db.Files var sameOriginFiles = await db.Files
.Where(f => f.ObjectId == file.ObjectId && f.Id != file.Id) .Where(f => f.StorageId == file.StorageId && f.Id != file.Id)
.Select(f => f.Id) .Select(f => f.Id)
.ToListAsync(); .ToListAsync();
@@ -575,16 +499,16 @@ public class FileService(
return; return;
} }
var dest = await GetRemoteStorageConfig(primaryReplica.PoolId.Value); var dest = await GetRemoteStorageConfig(file.PoolId.Value);
if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {primaryReplica.PoolId}"); if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}");
var client = CreateMinioClient(dest); var client = CreateMinioClient(dest);
if (client is null) if (client is null)
throw new InvalidOperationException( throw new InvalidOperationException(
$"Failed to configure client for remote destination '{primaryReplica.PoolId}'" $"Failed to configure client for remote destination '{file.PoolId}'"
); );
var bucket = dest.Bucket; var bucket = dest.Bucket;
var objectId = primaryReplica.StorageId; var objectId = file.StorageId ?? file.Id;
await client.RemoveObjectAsync( await client.RemoveObjectAsync(
new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId) new RemoveObjectArgs().WithBucket(bucket).WithObject(objectId)
@@ -617,55 +541,36 @@ public class FileService(
logger.LogWarning("Failed to delete thumbnail of file {fileId}", file.Id); 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) public async Task DeleteFileDataBatchAsync(List<SnCloudFile> files)
{ {
files = files.Where(f => f.ObjectId != null).ToList(); files = files.Where(f => f.PoolId.HasValue).ToList();
var objectIds = files.Select(f => f.ObjectId).Distinct().ToList(); foreach (var fileGroup in files.GroupBy(f => f.PoolId!.Value))
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(poolGroup.Key); var dest = await GetRemoteStorageConfig(fileGroup.Key);
if (dest is null) if (dest is null)
throw new InvalidOperationException($"No remote storage configured for pool {poolGroup.Key}"); throw new InvalidOperationException($"No remote storage configured for pool {fileGroup.Key}");
var client = CreateMinioClient(dest); var client = CreateMinioClient(dest);
if (client is null) if (client is null)
throw new InvalidOperationException( throw new InvalidOperationException(
$"Failed to configure client for remote destination '{poolGroup.Key}'" $"Failed to configure client for remote destination '{fileGroup.Key}'"
); );
List<string> objectsToDelete = []; List<string> objectsToDelete = [];
foreach (var replica in poolGroup) foreach (var file in fileGroup)
{ {
var file = files.First(f => f.ObjectId == replica.ObjectId); objectsToDelete.Add(file.StorageId ?? file.Id);
objectsToDelete.Add(replica.StorageId); if (file.HasCompression) objectsToDelete.Add(file.StorageId ?? file.Id + ".compressed");
if (file.HasCompression) objectsToDelete.Add(replica.StorageId + ".compressed"); if (file.HasThumbnail) objectsToDelete.Add(file.StorageId ?? file.Id + ".thumbnail");
if (file.HasThumbnail) objectsToDelete.Add(replica.StorageId + ".thumbnail");
} }
await client.RemoveObjectsAsync( await client.RemoveObjectsAsync(
new RemoveObjectsArgs().WithBucket(dest.Bucket).WithObjects(objectsToDelete) 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) private async Task<SnFileBundle?> GetBundleAsync(Guid id, Guid accountId)
@@ -702,7 +607,7 @@ public class FileService(
return await GetRemoteStorageConfig(id); return await GetRemoteStorageConfig(id);
} }
public static IMinioClient? CreateMinioClient(RemoteStorageConfig dest) public IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
{ {
var client = new MinioClient() var client = new MinioClient()
.WithEndpoint(dest.Endpoint) .WithEndpoint(dest.Endpoint)
@@ -715,16 +620,72 @@ public class FileService(
internal async Task _PurgeCacheAsync(string fileId) internal async Task _PurgeCacheAsync(string fileId)
{ {
var cacheKey = string.Concat(CacheKeyPrefix, fileId); var cacheKey = $"{CacheKeyPrefix}{fileId}";
await cache.RemoveAsync(cacheKey); await cache.RemoveAsync(cacheKey);
} }
private async Task _PurgeCacheRangeAsync(IEnumerable<string> fileIds) internal async Task _PurgeCacheRangeAsync(IEnumerable<string> fileIds)
{ {
var tasks = fileIds.Select(_PurgeCacheAsync); var tasks = fileIds.Select(_PurgeCacheAsync);
await Task.WhenAll(tasks); 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) private static bool IsIgnoredField(string fieldName)
{ {
var gpsFields = new[] var gpsFields = new[]
@@ -748,6 +709,8 @@ public class FileService(
.Where(f => f.AccountId == accountId && f.IsMarkedRecycle) .Where(f => f.AccountId == accountId && f.IsMarkedRecycle)
.ToListAsync(); .ToListAsync();
var count = files.Count; var count = files.Count;
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
await Task.WhenAll(tasks);
var fileIds = files.Select(f => f.Id).ToList(); var fileIds = files.Select(f => f.Id).ToList();
await _PurgeCacheRangeAsync(fileIds); await _PurgeCacheRangeAsync(fileIds);
db.RemoveRange(files); db.RemoveRange(files);
@@ -761,6 +724,8 @@ public class FileService(
.Where(f => f.AccountId == accountId && fileIds.Contains(f.Id)) .Where(f => f.AccountId == accountId && fileIds.Contains(f.Id))
.ToListAsync(); .ToListAsync();
var count = files.Count; var count = files.Count;
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
await Task.WhenAll(tasks);
var fileIdsList = files.Select(f => f.Id).ToList(); var fileIdsList = files.Select(f => f.Id).ToList();
await _PurgeCacheRangeAsync(fileIdsList); await _PurgeCacheRangeAsync(fileIdsList);
db.RemoveRange(files); db.RemoveRange(files);
@@ -770,16 +735,12 @@ public class FileService(
public async Task<int> DeletePoolRecycledFilesAsync(Guid poolId) 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 var files = await db.Files
.Where(f => fileIdsWithReplicas.Contains(f.Id) && f.IsMarkedRecycle) .Where(f => f.PoolId == poolId && f.IsMarkedRecycle)
.ToListAsync(); .ToListAsync();
var count = files.Count; var count = files.Count;
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
await Task.WhenAll(tasks);
var fileIds = files.Select(f => f.Id).ToList(); var fileIds = files.Select(f => f.Id).ToList();
await _PurgeCacheRangeAsync(fileIds); await _PurgeCacheRangeAsync(fileIds);
db.RemoveRange(files); db.RemoveRange(files);
@@ -793,6 +754,8 @@ public class FileService(
.Where(f => f.IsMarkedRecycle) .Where(f => f.IsMarkedRecycle)
.ToListAsync(); .ToListAsync();
var count = files.Count; var count = files.Count;
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
await Task.WhenAll(tasks);
var fileIds = files.Select(f => f.Id).ToList(); var fileIds = files.Select(f => f.Id).ToList();
await _PurgeCacheRangeAsync(fileIds); await _PurgeCacheRangeAsync(fileIds);
db.RemoveRange(files); db.RemoveRange(files);
@@ -800,43 +763,25 @@ public class FileService(
return count; return count;
} }
public async Task SetPublicAsync(string fileId) public async Task<string> CreateFastUploadLinkAsync(SnCloudFile file)
{ {
var existingPermission = await db.FilePermissions if (file.PoolId is null) throw new InvalidOperationException("Pool ID is null");
.FirstOrDefaultAsync(p =>
p.FileId == fileId &&
p.SubjectType == SnFilePermissionType.Anyone &&
p.Permission == SnFilePermissionLevel.Read);
if (existingPermission != null) var dest = await GetRemoteStorageConfig(file.PoolId.Value);
return; 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}'"
);
var permission = new SnFilePermission var url = await client.PresignedPutObjectAsync(
{ new PresignedPutObjectArgs()
Id = Guid.NewGuid(), .WithBucket(dest.Bucket)
FileId = fileId, .WithObject(file.Id)
SubjectType = SnFilePermissionType.Anyone, .WithExpiry(60 * 60 * 24)
SubjectId = string.Empty, );
Permission = SnFilePermissionLevel.Read return url;
};
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();
} }
} }
@@ -848,12 +793,13 @@ file class UpdatableCloudFile(SnCloudFile file)
public Dictionary<string, object?>? UserMeta { get; set; } = file.UserMeta; public Dictionary<string, object?>? UserMeta { get; set; } = file.UserMeta;
public bool IsMarkedRecycle { get; set; } = file.IsMarkedRecycle; public bool IsMarkedRecycle { get; set; } = file.IsMarkedRecycle;
public Action<UpdateSettersBuilder<SnCloudFile>> ToSetPropertyCalls() public Expression<Func<SetPropertyCalls<SnCloudFile>, SetPropertyCalls<SnCloudFile>>> ToSetPropertyCalls()
{ {
var userMeta = UserMeta ?? []; var userMeta = UserMeta ?? [];
return setter => setter return setter => setter
.SetProperty(f => f.Name, Name) .SetProperty(f => f.Name, Name)
.SetProperty(f => f.Description, Description) .SetProperty(f => f.Description, Description)
.SetProperty(f => f.FileMeta, FileMeta)
.SetProperty(f => f.UserMeta, userMeta) .SetProperty(f => f.UserMeta, userMeta)
.SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle); .SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle);
} }

View File

@@ -1,3 +1,4 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using Grpc.Core; using Grpc.Core;
@@ -6,7 +7,7 @@ namespace DysonNetwork.Drive.Storage
{ {
public class FileServiceGrpc(FileService fileService) : Shared.Proto.FileService.FileServiceBase public class FileServiceGrpc(FileService fileService) : Shared.Proto.FileService.FileServiceBase
{ {
public override async Task<CloudFile> GetFile(GetFileRequest request, ServerCallContext context) public override async Task<Shared.Proto.CloudFile> GetFile(GetFileRequest request, ServerCallContext context)
{ {
var file = await fileService.GetFileAsync(request.Id); var file = await fileService.GetFileAsync(request.Id);
return file?.ToProtoValue() ?? throw new RpcException(new Status(StatusCode.NotFound, "File not found")); return file?.ToProtoValue() ?? throw new RpcException(new Status(StatusCode.NotFound, "File not found"));
@@ -18,7 +19,7 @@ namespace DysonNetwork.Drive.Storage
return new GetFileBatchResponse { Files = { files.Select(f => f.ToProtoValue()) } }; return new GetFileBatchResponse { Files = { files.Select(f => f.ToProtoValue()) } };
} }
public override async Task<CloudFile> UpdateFile(UpdateFileRequest request, public override async Task<Shared.Proto.CloudFile> UpdateFile(UpdateFileRequest request,
ServerCallContext context) ServerCallContext context)
{ {
var file = await fileService.GetFileAsync(request.File.Id); var file = await fileService.GetFileAsync(request.File.Id);
@@ -40,22 +41,31 @@ namespace DysonNetwork.Drive.Storage
return new Empty(); 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) public override async Task<Empty> PurgeCache(PurgeCacheRequest request, ServerCallContext context)
{ {
await fileService._PurgeCacheAsync(request.FileId); await fileService._PurgeCacheAsync(request.FileId);
return new Empty(); 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();
}
} }
} }

View File

@@ -3,8 +3,8 @@ using DysonNetwork.Drive.Billing;
using DysonNetwork.Drive.Index; using DysonNetwork.Drive.Index;
using DysonNetwork.Drive.Storage.Model; using DysonNetwork.Drive.Storage.Model;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Networking;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -64,10 +64,7 @@ public class FileUploadController(
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
// Check if a file with the same hash already exists // Check if a file with the same hash already exists
var existingFile = await db.Files var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Hash == request.Hash);
.Include(f => f.Object)
.Where(f => f.Object != null && f.Object.Hash == request.Hash)
.FirstOrDefaultAsync();
if (existingFile != null) if (existingFile != null)
{ {
// Create the file index if a path is provided, even for existing files // Create the file index if a path is provided, even for existing files

View File

@@ -8,8 +8,8 @@ public static class FileUploadedEvent
public record FileUploadedEventPayload( public record FileUploadedEventPayload(
string FileId, string FileId,
Guid RemoteId, Guid RemoteId,
string? StorageId, string StorageId,
string? ContentType, string ContentType,
string ProcessingFilePath, string ProcessingFilePath,
bool IsTempFile bool IsTempFile
); );

View File

@@ -161,6 +161,7 @@ public class PersistentTask : ModelBase
public long? EstimatedDurationSeconds { get; set; } public long? EstimatedDurationSeconds { get; set; }
} }
// Backward compatibility - UploadTask inherits from PersistentTask
public class PersistentUploadTask : PersistentTask public class PersistentUploadTask : PersistentTask
{ {
public PersistentUploadTask() public PersistentUploadTask()

View File

@@ -1,8 +0,0 @@
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;
}

View File

@@ -664,54 +664,16 @@ public class PersistentTaskService(
if (cachedTask is not null) if (cachedTask is not null)
return cachedTask; return cachedTask;
var baseTask = await db.Tasks var task = await db.Tasks
.FirstOrDefaultAsync(t => t.TaskId == taskId && t.Type == TaskType.FileUpload && t.Status == TaskStatus.InProgress); .OfType<PersistentUploadTask>()
.FirstOrDefaultAsync(t => t.TaskId == taskId && t.Status == TaskStatus.InProgress);
if (baseTask is null) if (task is not null)
return null; await SetCacheAsync(task);
var task = ConvertToUploadTask(baseTask);
await SetCacheAsync(task);
return 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> /// <summary>
/// Updates chunk upload progress /// Updates chunk upload progress
/// </summary> /// </summary>
@@ -735,7 +697,8 @@ public class PersistentTaskService(
// Use ExecuteUpdateAsync to update the Parameters dictionary directly // Use ExecuteUpdateAsync to update the Parameters dictionary directly
var updatedRows = await db.Tasks var updatedRows = await db.Tasks
.Where(t => t.TaskId == taskId && t.Type == TaskType.FileUpload) .OfType<PersistentUploadTask>()
.Where(t => t.TaskId == taskId)
.ExecuteUpdateAsync(setters => setters .ExecuteUpdateAsync(setters => setters
.SetProperty(t => t.Parameters, ParameterHelper.Untyped(parameters)) .SetProperty(t => t.Parameters, ParameterHelper.Untyped(parameters))
.SetProperty(t => t.LastActivity, now) .SetProperty(t => t.LastActivity, now)
@@ -791,7 +754,7 @@ public class PersistentTaskService(
int limit = 50 int limit = 50
) )
{ {
var query = db.Tasks.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId); var query = db.Tasks.OfType<PersistentUploadTask>().Where(t => t.AccountId == accountId);
// Apply status filter // Apply status filter
if (status.HasValue) if (status.HasValue)
@@ -803,9 +766,19 @@ public class PersistentTaskService(
var totalCount = await query.CountAsync(); var totalCount = await query.CountAsync();
// Apply sorting // Apply sorting
IOrderedQueryable<PersistentTask> orderedQuery; IOrderedQueryable<PersistentUploadTask> orderedQuery;
switch (sortBy?.ToLower()) 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": case "created":
orderedQuery = sortDescending orderedQuery = sortDescending
? query.OrderByDescending(t => t.CreatedAt) ? query.OrderByDescending(t => t.CreatedAt)
@@ -825,27 +798,11 @@ public class PersistentTaskService(
} }
// Apply pagination // Apply pagination
var baseTasks = await orderedQuery var items = await orderedQuery
.Skip(offset) .Skip(offset)
.Take(limit) .Take(limit)
.ToListAsync(); .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); return (items, totalCount);
} }
@@ -854,12 +811,11 @@ public class PersistentTaskService(
/// </summary> /// </summary>
public async Task<UserUploadStats> GetUserUploadStatsAsync(Guid accountId) public async Task<UserUploadStats> GetUserUploadStatsAsync(Guid accountId)
{ {
var baseTasks = await db.Tasks var tasks = await db.Tasks
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId) .OfType<PersistentUploadTask>()
.Where(t => t.AccountId == accountId)
.ToListAsync(); .ToListAsync();
var tasks = ConvertToUploadTasks(baseTasks);
var stats = new UserUploadStats var stats = new UserUploadStats
{ {
TotalTasks = tasks.Count, TotalTasks = tasks.Count,
@@ -894,7 +850,8 @@ public class PersistentTaskService(
public async Task<int> CleanupUserFailedTasksAsync(Guid accountId) public async Task<int> CleanupUserFailedTasksAsync(Guid accountId)
{ {
var failedTasks = await db.Tasks var failedTasks = await db.Tasks
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId && .OfType<PersistentUploadTask>()
.Where(t => t.AccountId == accountId &&
(t.Status == TaskStatus.Failed || t.Status == TaskStatus.Expired)) (t.Status == TaskStatus.Failed || t.Status == TaskStatus.Expired))
.ToListAsync(); .ToListAsync();
@@ -926,13 +883,12 @@ public class PersistentTaskService(
/// </summary> /// </summary>
public async Task<List<PersistentUploadTask>> GetRecentUserTasksAsync(Guid accountId, int limit = 10) public async Task<List<PersistentUploadTask>> GetRecentUserTasksAsync(Guid accountId, int limit = 10)
{ {
var baseTasks = await db.Tasks return await db.Tasks
.Where(t => t.Type == TaskType.FileUpload && t.AccountId == accountId) .OfType<PersistentUploadTask>()
.Where(t => t.AccountId == accountId)
.OrderByDescending(t => t.LastActivity) .OrderByDescending(t => t.LastActivity)
.Take(limit) .Take(limit)
.ToListAsync(); .ToListAsync();
return ConvertToUploadTasks(baseTasks);
} }
/// <summary> /// <summary>

View File

@@ -10,7 +10,10 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"Registrar": "127.0.0.1:2379",
"Cache": "127.0.0.1:6379",
"Queue": "127.0.0.1:4222"
}, },
"Authentication": { "Authentication": {
"Schemes": { "Schemes": {
@@ -74,6 +77,11 @@
"FromName": "Alphabot", "FromName": "Alphabot",
"SubjectPrefix": "Solar Network" "SubjectPrefix": "Solar Network"
}, },
"RealtimeChat": {
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
"ApiKey": "APIs6TiL8wj3A4j",
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
},
"GeoIp": { "GeoIp": {
"DatabasePath": "./Keys/GeoLite2-City.mmdb" "DatabasePath": "./Keys/GeoLite2-City.mmdb"
}, },
@@ -107,19 +115,10 @@
} }
}, },
"Cache": { "Cache": {
"Serializer": "JSON" "Serializer": "MessagePack"
},
"AccessToken": {
"Secret": "dyson-network-default-access-token-secret-change-in-production"
}, },
"KnownProxies": [ "KnownProxies": [
"127.0.0.1", "127.0.0.1",
"::1" "::1"
], ]
"FileReanalysis": {
"Enabled": true,
"ValidateCompression": true,
"ValidateThumbnails": true,
"DelayMs": 10000
}
} }

View File

@@ -1,14 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Gateway.Configuration;
[ApiController]
[Route("config")]
public class ConfigurationController(IConfiguration configuration) : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(configuration.GetSection("Client").Get<Dictionary<string, object>>());
[HttpGet("site")]
public IActionResult GetSiteUrl() => Ok(configuration["SiteUrl"]);
}

View File

@@ -1,54 +0,0 @@
namespace DysonNetwork.Gateway.Configuration;
public class GatewayEndpointsOptions
{
public const string SectionName = "Endpoints";
/// <summary>
/// List of all services that the gateway should manage.
/// If not specified, defaults to the built-in service list.
/// </summary>
public List<string>? ServiceNames { get; set; }
/// <summary>
/// List of core services that are essential for the application to function.
/// If not specified, defaults to the built-in core service list.
/// </summary>
public List<string>? CoreServiceNames { get; set; }
/// <summary>
/// Default service names used when no configuration is provided.
/// </summary>
public static readonly string[] DefaultServiceNames =
[
"ring",
"pass",
"drive",
"sphere",
"develop",
"insight",
"zone",
"messager"
];
/// <summary>
/// Default core service names used when no configuration is provided.
/// </summary>
public static readonly string[] DefaultCoreServiceNames =
[
"ring",
"pass",
"drive",
"sphere"
];
/// <summary>
/// Gets the effective service names, using configuration if available, otherwise defaults.
/// </summary>
public string[] GetServiceNames() => ServiceNames?.ToArray() ?? DefaultServiceNames;
/// <summary>
/// Gets the effective core service names, using configuration if available, otherwise defaults.
/// </summary>
public string[] GetCoreServiceNames() => CoreServiceNames?.ToArray() ?? DefaultCoreServiceNames;
}

View File

@@ -1,23 +0,0 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"]
RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj"
COPY . .
WORKDIR "/src/DysonNetwork.Gateway"
RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"]

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<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>
</PackageReference>
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,53 +0,0 @@
namespace DysonNetwork.Gateway.Health;
public abstract class GatewayConstant
{
// Default service names used when no configuration is provided
private static readonly string[] DefaultServiceNames =
[
"ring",
"pass",
"drive",
"sphere",
"develop",
"insight",
"zone",
"messager"
];
// Default core service names used when no configuration is provided
private static readonly string[] DefaultCoreServiceNames =
[
"ring",
"pass",
"drive",
"sphere"
];
// Configuration-driven service names with fallback to defaults
public static string[] ServiceNames { get; private set; } = DefaultServiceNames;
// Configuration-driven core service names with fallback to defaults
public static string[] CoreServiceNames { get; private set; } = DefaultCoreServiceNames;
/// <summary>
/// Initializes the service names from configuration options.
/// This method should be called during application startup.
/// </summary>
/// <param name="options">The gateway endpoints options containing configuration</param>
public static void InitializeFromConfiguration(DysonNetwork.Gateway.Configuration.GatewayEndpointsOptions options)
{
ServiceNames = options.GetServiceNames();
CoreServiceNames = options.GetCoreServiceNames();
}
/// <summary>
/// Resets the service names to their default values.
/// Useful for testing or when configuration is not available.
/// </summary>
public static void ResetToDefaults()
{
ServiceNames = DefaultServiceNames;
CoreServiceNames = DefaultCoreServiceNames;
}
}

View File

@@ -1,60 +0,0 @@
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);
}
}
}

View File

@@ -1,35 +0,0 @@
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);
}
}

View File

@@ -1,112 +0,0 @@
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);
}
/// <summary>
/// Reinitializes the store with new service names from configuration.
/// This method should be called when configuration changes.
/// </summary>
/// <param name="serviceNames">The new service names to track</param>
public void ReinitializeServices(string[] serviceNames)
{
lock (_lock)
{
// Preserve existing health states for services that still exist
var existingStates = new Dictionary<string, ServiceHealthState>(_services);
_services.Clear();
foreach (var name in serviceNames)
{
// Use existing state if available, otherwise create new unhealthy state
if (existingStates.TryGetValue(name, out var existingState))
{
_services[name] = existingState;
}
else
{
_services[name] = new ServiceHealthState(
name,
IsHealthy: false,
LastChecked: SystemClock.Instance.GetCurrentInstant(),
Error: "Not checked yet"
);
}
}
RecalculateLocked();
}
}
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()
);
}
}

View File

@@ -1,14 +0,0 @@
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);
}
}

View File

@@ -1,204 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
using DysonNetwork.Gateway.Configuration;
using DysonNetwork.Gateway.Health;
using DysonNetwork.Shared.Networking;
using Yarp.ReverseProxy.Configuration;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Options;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue, enableGrpc: false);
builder.Services.AddSingleton<GatewayReadinessStore>();
builder.Services.AddHostedService<GatewayHealthAggregator>();
// Add configuration options for gateway endpoints
builder.Services.Configure<DysonNetwork.Gateway.Configuration.GatewayEndpointsOptions>(
builder.Configuration.GetSection(DysonNetwork.Gateway.Configuration.GatewayEndpointsOptions.SectionName));
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithExposedHeaders("X-Total", "X-NotReady");
});
});
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed", context =>
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ip,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 120, // 120 requests...
Window = TimeSpan.FromMinutes(1), // ...per minute per IP
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 10 // allow short bursts instead of instant 503s
});
});
options.OnRejected = async (context, token) =>
{
// 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);
// Respond to the client
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync(
"Rate limit exceeded. Try again later.", token);
};
});
var specialRoutes = new[]
{
new RouteConfig
{
RouteId = "ring-ws",
ClusterId = "ring",
Match = new RouteMatch { Path = "/ws" }
},
new RouteConfig
{
RouteId = "pass-openid",
ClusterId = "pass",
Match = new RouteMatch { Path = "/.well-known/openid-configuration" }
},
new RouteConfig
{
RouteId = "pass-jwks",
ClusterId = "pass",
Match = new RouteMatch { Path = "/.well-known/jwks" }
},
new RouteConfig
{
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 = GatewayConstant.ServiceNames.Select(serviceName =>
{
var apiPath = serviceName switch
{
_ => $"/{serviceName}"
};
return new RouteConfig
{
RouteId = $"{serviceName}-api",
ClusterId = serviceName,
Match = new RouteMatch { Path = $"{apiPath}/{{**catch-all}}" },
Transforms =
[
new Dictionary<string, string> { { "PathRemovePrefix", apiPath } },
new Dictionary<string, string> { { "PathPrefix", "/api" } }
]
};
});
var swaggerRoutes = GatewayConstant.ServiceNames.Select(serviceName => new RouteConfig
{
RouteId = $"{serviceName}-swagger",
ClusterId = serviceName,
Match = new RouteMatch { Path = $"/swagger/{serviceName}/{{**catch-all}}" },
Transforms =
[
new Dictionary<string, string> { { "PathRemovePrefix", $"/swagger/{serviceName}" } },
new Dictionary<string, string> { { "PathPrefix", "/swagger" } }
]
});
var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray();
var clusters = GatewayConstant.ServiceNames.Select(serviceName => new ClusterConfig
{
ClusterId = serviceName,
HealthCheck = new HealthCheckConfig
{
Active = new ActiveHealthCheckConfig
{
Enabled = true,
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5),
Path = "/health"
},
Passive = new PassiveHealthCheckConfig
{
Enabled = true
}
},
Destinations = new Dictionary<string, DestinationConfig>
{
{ "destination1", new DestinationConfig { Address = $"http://{serviceName}" } }
}
}).ToArray();
builder.Services
.AddReverseProxy()
.LoadFromMemory(routes, clusters)
.AddServiceDiscoveryDestinationResolver();
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();
// Initialize GatewayConstant with configuration values
var gatewayEndpointsOptions = app.Services
.GetRequiredService<IOptions<GatewayEndpointsOptions>>().Value;
GatewayConstant.InitializeFromConfiguration(gatewayEndpointsOptions);
// Reinitialize the readiness store with configured service names
var readinessStore = app.Services.GetRequiredService<GatewayReadinessStore>();
readinessStore.ReinitializeServices(GatewayConstant.ServiceNames);
var forwardedHeadersOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
};
forwardedHeadersOptions.KnownIPNetworks.Clear();
forwardedHeadersOptions.KnownProxies.Clear();
app.UseForwardedHeaders(forwardedHeadersOptions);
app.UseCors();
app.UseMiddleware<GatewayReadinessMiddleware>();
app.MapReverseProxy().RequireRateLimiting("fixed");
app.MapControllers();
app.Run();

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,20 +0,0 @@
using DysonNetwork.Shared.Data;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Gateway;
[ApiController]
[Route("/version")]
public class VersionController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new AppVersion
{
Version = ThisAssembly.AssemblyVersion,
Commit = ThisAssembly.GitCommitId,
UpdateDate = ThisAssembly.GitCommitDate
});
}
}

View File

@@ -1,34 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Cache": {
"Serializer": "JSON"
},
"AllowedHosts": "*",
"SiteUrl": "http://localhost:3000",
"Client": {
"SomeSetting": "SomeValue"
},
"Endpoints": {
"ServiceNames": [
"ring",
"pass",
"drive",
"sphere",
"develop",
"insight",
"zone",
"messager"
],
"CoreServiceNames": [
"ring",
"pass",
"drive",
"sphere"
]
}
}

View File

@@ -1,7 +0,0 @@
{
"version": "1.0",
"publicReleaseRefSpec": ["^refs/heads/main$"],
"cloudBuild": {
"setVersionVariables": true
}
}

View File

@@ -15,10 +15,6 @@ public class AppDatabase(
public DbSet<SnThinkingThought> ThinkingThoughts { get; set; } public DbSet<SnThinkingThought> ThinkingThoughts { get; set; }
public DbSet<SnUnpaidAccount> UnpaidAccounts { 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) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseNpgsql( optionsBuilder.UseNpgsql(
@@ -42,8 +38,6 @@ public class AppDatabase(
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Ignore<SnAccount>();
modelBuilder.ApplySoftDeleteFilters(); modelBuilder.ApplySoftDeleteFilters();
} }
} }

View File

@@ -1,9 +1,6 @@
#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 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 USER app
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080

View File

@@ -7,27 +7,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.4.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.33.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" /> <PackageReference Include="Microsoft.SemanticKernel" Version="1.67.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.Connectors.Ollama" Version="1.66.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" 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" Version="3.15.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" /> <PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" /> <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
<PackageReference Include="System.ServiceModel.Syndication" Version="10.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -38,8 +28,4 @@
<Folder Include="Controllers\" /> <Folder Include="Controllers\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Protobuf Remove="..\DysonNetwork.Shared\Proto\**" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,358 +0,0 @@
// <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
}
}
}

View File

@@ -1,114 +0,0 @@
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");
}
}
}

View File

@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using DysonNetwork.Insight; using DysonNetwork.Insight;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Models.Embed;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -21,7 +20,7 @@ namespace DysonNetwork.Insight.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.1") .HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -144,171 +143,6 @@ namespace DysonNetwork.Insight.Migrations
b.ToTable("unpaid_accounts", (string)null); 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 => modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
@@ -320,35 +154,6 @@ namespace DysonNetwork.Insight.Migrations
b.Navigation("Sequence"); 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 #pragma warning restore 612, 618
} }
} }

View File

@@ -1,13 +1,15 @@
using DysonNetwork.Insight; using DysonNetwork.Insight;
using DysonNetwork.Insight.Startup; using DysonNetwork.Insight.Startup;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Networking; using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Registry; using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); builder.Services.Configure<ServiceRegistrationOptions>(opts => { opts.Name = "insight"; });
builder.AddServiceDefaults("insight");
builder.ConfigureAppKestrel(builder.Configuration); builder.ConfigureAppKestrel(builder.Configuration);
@@ -19,8 +21,6 @@ builder.Services.AddAppBusinessServices();
builder.Services.AddAppScheduledJobs(); builder.Services.AddAppScheduledJobs();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();
builder.Services.AddAccountService();
builder.Services.AddSphereService();
builder.Services.AddThinkingServices(builder.Configuration); builder.Services.AddThinkingServices(builder.Configuration);
builder.AddSwaggerManifest( builder.AddSwaggerManifest(

View File

@@ -1,33 +0,0 @@
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
};
}
}

View File

@@ -1,90 +0,0 @@
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;
}
}

View File

@@ -1,55 +0,0 @@
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;
}
}

View File

@@ -1,323 +0,0 @@
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;
}

View File

@@ -1,28 +0,0 @@
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.");
}
}

View File

@@ -1,49 +0,0 @@
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 };
}
}

View File

@@ -1,5 +1,4 @@
using DysonNetwork.Insight.Reader; using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Networking;
namespace DysonNetwork.Insight.Startup; namespace DysonNetwork.Insight.Startup;
@@ -18,11 +17,6 @@ public static class ApplicationConfiguration
app.MapControllers(); app.MapControllers();
app.MapGrpcService<WebReaderGrpcService>();
app.MapGrpcService<WebArticleGrpcService>();
app.MapGrpcService<WebFeedGrpcService>();
app.MapGrpcReflectionService();
return app; return app;
} }
} }

View File

@@ -1,4 +1,3 @@
using DysonNetwork.Insight.Reader;
using DysonNetwork.Insight.Thought; using DysonNetwork.Insight.Thought;
using Quartz; using Quartz;
@@ -19,20 +18,6 @@ public static class ScheduledJobsConfiguration
.WithIntervalInMinutes(5) .WithIntervalInMinutes(5)
.RepeatForever()) .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); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

View File

@@ -1,9 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Insight.Reader;
using DysonNetwork.Insight.Thought; using DysonNetwork.Insight.Thought;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry; using DysonNetwork.Shared.Registry;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
using NodaTime; using NodaTime;
@@ -13,65 +11,60 @@ namespace DysonNetwork.Insight.Startup;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
extension(IServiceCollection services) public static IServiceCollection AddAppServices(this IServiceCollection services)
{ {
public IServiceCollection AddAppServices() services.AddDbContext<AppDatabase>();
services.AddHttpContextAccessor();
services.AddHttpClient();
// Register gRPC services
services.AddGrpc(options =>
{ {
services.AddDbContext<AppDatabase>(); options.EnableDetailedErrors = true; // Will be adjusted in Program.cs
services.AddHttpContextAccessor(); options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
});
services.AddGrpcReflection();
services.AddHttpClient(); // Register gRPC services
// Register gRPC services // Register OIDC services
services.AddGrpc(options => services.AddControllers().AddJsonOptions(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()
{ {
services.AddAuthorization(); options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
return services; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
} options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
public IServiceCollection AddAppFlushHandlers() options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
{ });
services.AddSingleton<FlushBufferService>();
return services; return services;
} }
public IServiceCollection AddAppBusinessServices() public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
{ {
return services; services.AddAuthorization();
} return services;
}
public IServiceCollection AddThinkingServices(IConfiguration configuration) public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
{ {
services.AddSingleton<ThoughtProvider>(); services.AddSingleton<FlushBufferService>();
services.AddScoped<ThoughtService>();
services.AddScoped<Reader.WebFeedService>();
services.AddScoped<Reader.WebReaderService>();
return services; 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;
} }
} }

View File

@@ -1,5 +1,6 @@
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Microsoft.IdentityModel.Tokens;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
namespace DysonNetwork.Insight.Thought.Plugins; namespace DysonNetwork.Insight.Thought.Plugins;
@@ -23,6 +24,6 @@ public class SnAccountKernelPlugin(
var request = new LookupAccountBatchRequest(); var request = new LookupAccountBatchRequest();
request.Names.Add(username); request.Names.Add(username);
var response = await accountClient.LookupAccountBatchAsync(request); var response = await accountClient.LookupAccountBatchAsync(request);
return response.Accounts.Count == 0 ? null : SnAccount.FromProtoValue(response.Accounts[0]); return response.Accounts.IsNullOrEmpty() ? null : SnAccount.FromProtoValue(response.Accounts[0]);
} }
} }

View File

@@ -27,7 +27,6 @@ public class ThoughtProvider
private readonly PostService.PostServiceClient _postClient; private readonly PostService.PostServiceClient _postClient;
private readonly AccountService.AccountServiceClient _accountClient; private readonly AccountService.AccountServiceClient _accountClient;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly ILogger<ThoughtProvider> _logger;
private readonly Dictionary<string, Kernel> _kernels = new(); private readonly Dictionary<string, Kernel> _kernels = new();
private readonly Dictionary<string, string> _serviceProviders = new(); private readonly Dictionary<string, string> _serviceProviders = new();
@@ -38,11 +37,9 @@ public class ThoughtProvider
public ThoughtProvider( public ThoughtProvider(
IConfiguration configuration, IConfiguration configuration,
PostService.PostServiceClient postServiceClient, PostService.PostServiceClient postServiceClient,
AccountService.AccountServiceClient accountServiceClient, AccountService.AccountServiceClient accountServiceClient
ILogger<ThoughtProvider> logger
) )
{ {
_logger = logger;
_postClient = postServiceClient; _postClient = postServiceClient;
_accountClient = accountServiceClient; _accountClient = accountServiceClient;
_configuration = configuration; _configuration = configuration;

View File

@@ -10,7 +10,10 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_insight;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" "App": "Host=localhost;Port=5432;Database=dyson_insight;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"Registrar": "127.0.0.1:2379",
"Cache": "127.0.0.1:6379",
"Queue": "127.0.0.1:4222"
}, },
"KnownProxies": [ "KnownProxies": [
"127.0.0.1", "127.0.0.1",
@@ -20,7 +23,7 @@
"Insecure": true "Insecure": true
}, },
"Cache": { "Cache": {
"Serializer": "JSON" "Serializer": "MessagePack"
}, },
"Thinking": { "Thinking": {
"DefaultService": "deepseek-chat", "DefaultService": "deepseek-chat",

View File

@@ -1,5 +0,0 @@
Keys
Uploads
DataProtection-Keys
.DS_Store

Some files were not shown because too many files have changed in this diff Show More