Compare commits
346 Commits
refactor/a
...
270c211cb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
270c211cb8
|
|||
|
74c8f3490d
|
|||
|
b364edc74b
|
|||
|
9addf38677
|
|||
|
a02ed10434
|
|||
|
aca28f9318
|
|||
|
c2f72993b7
|
|||
|
158cc75c5b
|
|||
|
fa2f53ff7a
|
|||
|
2cce5ebf80
|
|||
|
13b2e46ecc
|
|||
|
cbd68c9ae6
|
|||
|
b99b61e0f9
|
|||
|
94f4e68120
|
|||
|
d5510f7e4d
|
|||
|
c038ab9e3c
|
|||
|
e97719ec84
|
|||
|
40b8ea8eb8
|
|||
|
f9b4dd45d7
|
|||
|
a46de4662c
|
|||
|
fdd14b860e
|
|||
|
cb62df81e2
|
|||
|
46717e39a7
|
|||
|
344ed6e348
|
|||
|
a8b62fb0eb
|
|||
|
00b3087d6a
|
|||
|
78f3873a0c
|
|||
|
a7f4173df7
|
|||
|
f51c3c1724
|
|||
|
a92dc7e140
|
|||
|
c42befed6b
|
|||
|
2b95d58611
|
|||
|
726a752fbb
|
|||
|
2024972832
|
|||
|
d553ca2ca7
|
|||
|
aeef16495f
|
|||
|
9b26a2a7eb
|
|||
|
2317033dae
|
|||
|
fd6e9c9780
|
|||
|
af0a2ff493
|
|||
|
b142a71c32
|
|||
|
27e3cc853a
|
|||
|
590519c28f
|
|||
|
8ccf8100d4
|
|||
|
ec21a94921
|
|||
|
7b7a6c9218
|
|||
|
0e44d9c514
|
|||
|
e449e16d33
|
|||
|
3ce2b36c15
|
|||
|
f7388822e0
|
|||
|
3800dae8b7
|
|||
|
c62ed191f3
|
|||
|
8b77f0e0ad
|
|||
|
2b56c6f1e5
|
|||
|
ef02265ccd
|
|||
|
f4505d2ecc
|
|||
|
9d2242d331
|
|||
|
c806365a81
|
|||
|
bd1715c9a3
|
|||
|
0b0598712e
|
|||
|
92a4899e7c
|
|||
|
bdc8db3091
|
|||
|
a16da37221
|
|||
|
70a18b07ff
|
|||
|
98b8d5f33b
|
|||
|
2a35786204
|
|||
|
7016a0a943
|
|||
|
cad72502d9
|
|||
|
226a64df41
|
|||
|
75b8567a28
|
|||
|
3aa5561a07
|
|||
|
c0ebb496fe
|
|||
|
afccb27bd4
|
|||
|
6ed96780ab
|
|||
|
8e5cdfbc62
|
|||
|
1b774c1de6
|
|||
|
9b4cbade5c
|
|||
|
a52e54f672
|
|||
|
aa48d5e25d
|
|||
|
ce18b194a5
|
|||
|
382579a20e
|
|||
|
18d50346a9
|
|||
|
ac51bbde6c
|
|||
|
4ab0dcf1c2
|
|||
|
587066d847
|
|||
|
faa375042a
|
|||
|
65b6f3a606
|
|||
|
fa1a40c637
|
|||
|
d43ce7cb11
|
|||
|
92b28d830d
|
|||
|
1fa6c893a5
|
|||
|
ba57becba8
|
|||
|
4280168002
|
|||
|
a172128d84
|
|||
|
34e78294a1
|
|||
|
82afdb3922
|
|||
|
260b3e7bc6
|
|||
|
713777cd8a
|
|||
|
5cd09bc2d0
|
|||
|
861fc7cafa
|
|||
|
6313f15375
|
|||
|
337cc1be97
|
|||
|
9b4f61fcda
|
|||
|
6252988390
|
|||
|
aace3b48b1
|
|||
|
5a097c7518
|
|||
|
ba3be1e3bb
|
|||
|
6fd90c424d
|
|||
|
a0ac3b5820
|
|||
|
076bf347c8
|
|||
|
788326381f
|
|||
|
a035b23242
|
|||
|
b29f4fce4d
|
|||
|
5418489f77
|
|||
|
310f2c1497
|
|||
|
0ae8a2cfd4
|
|||
|
c69256bda6
|
|||
|
80ea44f2cc
|
|||
|
b5f9faa724
|
|||
|
05985e0852
|
|||
|
6814b5690e
|
|||
|
78447de1b6
|
|||
|
e54dcccad9
|
|||
|
429a08930f
|
|||
|
b94b288755
|
|||
|
1c50c2f822
|
|||
|
73700e7cfd
|
|||
|
bd2943345a
|
|||
|
1647aa2f1e
|
|||
|
b137021b1f
|
|||
|
ffca94f789
|
|||
|
e2b2bdd262
|
|||
|
ce715cd6b0
|
|||
|
f7b3926338
|
|||
|
68cd23d64f
|
|||
|
db7d994039
|
|||
|
741ed18ce5
|
|||
|
2bfb50cc71
|
|||
|
db98fa240e
|
|||
|
d96937aabc
|
|||
|
dc0be3467f
|
|||
|
6101de741f
|
|||
|
6c8ad05872
|
|||
|
f5b37e9419
|
|||
|
ce5f3434eb
|
|||
|
c08503d2f3
|
|||
|
c8fec66e07
|
|||
|
61b49377a7
|
|||
|
0123c74ab8
|
|||
|
637cc0cfa4
|
|||
|
94a0ec71da
|
|||
|
1351db5482
|
|||
|
3e98ac29b7
|
|||
|
09625335f0
|
|||
|
ee9ad6d87f
|
|||
|
67fc82a8fb
|
|||
|
58e79655e8
|
|||
|
f271681b5d
|
|||
|
3e838cfdb5
|
|||
|
e0e00d023f
|
|||
|
433230b495
|
|||
|
b8fa5f5f24
|
|||
|
091fbd857e
|
|||
|
bfa9bedeea
|
|||
|
74f8221be4
|
|||
|
6817ab6b56
|
|||
|
c74ab20236
|
|||
|
b9edf51f05
|
|||
|
74a9ca98ad
|
|||
|
4bd59f107b
|
|||
|
08f924f647
|
|||
|
5445df3b61
|
|||
|
a377ca2072
|
|||
|
623e7a5771
|
|||
|
0351a2b4fa
|
|||
|
322dee4453
|
|||
|
5e5f4528b9
|
|||
|
70fdc247e7
|
|||
|
8f5f1efa24
|
|||
|
0f15510ac6
|
|||
|
3ce457e9f9
|
|||
|
a9168dcdc5
|
|||
|
4ad63577ba
|
|||
|
47722cfd57
|
|||
|
b46a010e73
|
|||
|
ccd9dbcdbf
|
|||
|
0b65bf8dd7
|
|||
|
ab23f87a66
|
|||
|
8f1047ff5d
|
|||
|
43e50a00ce
|
|||
|
50133684c7
|
|||
|
befde25266
|
|||
|
437f49fb20
|
|||
|
c3b6358f33
|
|||
|
4347281fcd
|
|||
|
92cd6b5f7e
|
|||
|
cf6e534d02
|
|||
|
29c5971554
|
|||
|
cdfc3f6571
|
|||
|
f65a7360e2
|
|||
|
85e706335a
|
|||
|
fe74060df9
|
|||
|
e8d5f22395
|
|||
|
83fa2568aa
|
|||
|
bf1c8e0a85
|
|||
|
323fa8ee15
|
|||
|
e7a46e96ed
|
|||
|
3a0dee11a6
|
|||
|
43be47d526
|
|||
|
48067af034
|
|||
|
7e7e90ad24
|
|||
|
3af4069581
|
|||
|
609b130b4e
|
|||
|
93f7dfd379
|
|||
|
40325c6df5
|
|||
|
bbcaa27ac5
|
|||
|
19d833a522
|
|||
|
a94102e136
|
|||
|
fc693793fe
|
|||
|
8cfdabbae4
|
|||
|
985ff41c72
|
|||
|
a79ea4ac49
|
|||
|
7385caff9a
|
|||
|
15954dbfe2
|
|||
|
4ba6206c9d
|
|||
|
266b9e36e2
|
|||
|
e6aa61b03b
|
|||
|
0c09ef25ec
|
|||
|
dd5929c691
|
|||
|
cf87fdfb49
|
|||
|
ff03584518
|
|||
|
d6c37784e1
|
|||
|
46ebd92dc1
|
|||
|
7f8521bb40
|
|||
|
f01226d91a
|
|||
|
6cb6dee6be
|
|||
|
0e9caf67ff
|
|||
|
ca70bb5487
|
|||
|
59ed135f20
|
|||
|
6077f91529
|
|||
|
5c485bb1c3
|
|||
|
27d979d77b
|
|||
|
15687a0c32
|
|||
|
37ea882ef7
|
|||
|
e624c2bb3e
|
|||
|
9631cd3edd
|
|||
|
f4a659fce5
|
|||
|
1ded811b36
|
|||
|
32977d9580
|
|||
|
aaf29e7228
|
|||
|
658ef3bddf
|
|||
|
fc0bc936ce
|
|||
|
3850ae6a8e
|
|||
|
21c99567b4
|
|||
|
1315c7f4d4
|
|||
|
630a532d98
|
|||
|
b9bb180113
|
|||
|
04d74d0d70
|
|||
|
6a8a0ed491
|
|||
|
0f835845bf
|
|||
|
c5d8a8d07f
|
|||
|
95e2ba1136
|
|||
|
1176fde8b4
|
|||
|
e634968e00
|
|||
|
282a1dbddc
|
|||
|
c64adace24
|
|||
|
8ac0b28c66
|
|||
|
8f71d7f9e5
|
|||
|
c435e63917
|
|||
|
243159e4cc
|
|||
|
42dad7095a
|
|||
|
d1efcdede8
|
|||
|
47680475b3
|
|||
|
6632d43f32
|
|||
|
29c4dcd71c
|
|||
|
e7aa887715
|
|||
|
0f05633996
|
|||
|
966af08a33
|
|||
|
b25b90a074
|
|||
|
dcbefeaaab
|
|||
|
eb83a0392a
|
|||
|
85fefcf724
|
|||
|
d17c26a228
|
|||
|
2e5ef8ff94
|
|||
|
7a5f410e36
|
|||
|
0b4e8a9777
|
|||
|
30fd912281
|
|||
|
5bf58f0194
|
|||
|
8e3e3f09df
|
|||
|
fa24f14c05
|
|||
|
a93b633e84
|
|||
|
97a7b876db
|
|||
|
909fe173c2
|
|||
|
58a44e8af4
|
|||
|
1075177511
|
|||
|
78f8a9e638
|
|||
|
9ce31c4dd8
|
|||
|
e70d8371f8
|
|||
|
51b6f7309e
|
|||
|
d75876a772
|
|||
|
4910c3296b
|
|||
|
7b924fa075
|
|||
|
d69c9f9623
|
|||
|
a88d828e21
|
|||
|
14c93d372e
|
|||
|
adf371a72e
|
|||
|
c03f2472fa
|
|||
|
50efe62bac
|
|||
|
7bc94a9646
|
|||
|
d9fe1273b5
|
|||
|
ff9d490869
|
|||
|
266312e97e
|
|||
|
7087736e31
|
|||
|
82bf1608fd
|
|||
|
3b3287db0b
|
|||
|
4573d9395f
|
|||
|
a8c99b3128
|
|||
|
fdd7bd3c9d
|
|||
|
b785d0098b
|
|||
|
5b31357fe9
|
|||
|
d5a5721402
|
|||
|
204640a759
|
|||
|
e3657386cd
|
|||
|
f81e3dc9f4
|
|||
|
b2a0d25ffa
|
|||
|
e1459951c4
|
|||
|
a88843a4c2
|
|||
|
4d83c2de31
|
|||
|
f63c934cee
|
|||
|
001da9ae40
|
|||
|
4efbfa948a
|
|||
|
3458e85a8b
|
|||
|
3710169f8c
|
|||
|
9e4a58a8a0
|
|||
|
dc93991de2
|
|||
|
b0154e1a63
|
|||
|
66e14ffedb
|
|||
|
b152edb848
|
|||
|
2ace444dbb
|
|||
|
634958ffc5
|
|||
|
1e374a73c7
|
|||
|
cc59e046bd
|
|||
|
f3dcff2e4a
|
|||
|
1a5723c880
|
|||
|
96559a2c26
|
|||
| 366edfc14f |
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
3
.env
3
.env
@@ -33,3 +33,6 @@ SPHERE_IMAGE=sphere:latest
|
||||
|
||||
# Container image name for develop
|
||||
DEVELOP_IMAGE=develop:latest
|
||||
|
||||
# Container image name for gateway
|
||||
GATEWAY_IMAGE=gateway:latest
|
||||
|
||||
115
.github/workflows/docker-build.yml
vendored
115
.github/workflows/docker-build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Aspire Publish Workflow
|
||||
name: Build and Push Microservices
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,19 +7,82 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
determine-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.changes.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine changed services
|
||||
id: changes
|
||||
run: |
|
||||
files="${{ steps.changed-files.outputs.files }}"
|
||||
matrix="{\"include\":[]}"
|
||||
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone")
|
||||
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone")
|
||||
changed_services=()
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == DysonNetwork.Shared/* ]]; then
|
||||
changed_services=("${services[@]}")
|
||||
break
|
||||
fi
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then
|
||||
# check if service is already in changed_services
|
||||
if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then
|
||||
changed_services+=("${services[$i]}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ ${#changed_services[@]} -gt 0 ]; then
|
||||
json_objects=""
|
||||
for service in "${changed_services[@]}"; do
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "${services[$i]}" == "$service" ]]; then
|
||||
image="${images[$i]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
json_objects+="{\"service\":\"$service\",\"image\":\"$image\"},"
|
||||
done
|
||||
matrix="{\"include\":[${json_objects%,}]}"
|
||||
fi
|
||||
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
||||
|
||||
build-and-push:
|
||||
needs: determine-changes
|
||||
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -28,33 +91,13 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Aspire CLI
|
||||
run: dotnet tool install -g Aspire.Cli --prerelease
|
||||
|
||||
- name: Build and Publish Aspire Application
|
||||
run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish
|
||||
|
||||
- name: Tag and Push Images
|
||||
run: |
|
||||
IMAGES=( "sphere" "pass" "ring" "drive" "develop" )
|
||||
|
||||
for image in "${IMAGES[@]}"; do
|
||||
IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha"
|
||||
SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name
|
||||
|
||||
echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..."
|
||||
docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME
|
||||
docker push $IMAGE_NAME
|
||||
done
|
||||
|
||||
- name: Upload Aspire Publish Directory
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Build and push Docker image for ${{ matrix.service }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
name: aspire-publish-output
|
||||
path: ./publish/
|
||||
|
||||
- name: Upload Docker Compose file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-compose-output
|
||||
path: ./publish/docker-compose.yml
|
||||
context: .
|
||||
file: DysonNetwork.${{ matrix.service }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
.DS_Store
|
||||
/Keys/
|
||||
|
||||
613
API_WALLET_FUNDS.md
Normal file
613
API_WALLET_FUNDS.md
Normal 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
|
||||
@@ -1,76 +1,76 @@
|
||||
using Aspire.Hosting.Yarp.Transforms;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
// Database was configured separately in each service.
|
||||
// var database = builder.AddPostgres("database");
|
||||
var isDev = builder.Environment.IsDevelopment();
|
||||
|
||||
var cache = builder.AddRedis("cache");
|
||||
var queue = builder.AddNats("queue").WithJetStream();
|
||||
|
||||
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring")
|
||||
.WithReference(queue)
|
||||
.WithHttpHealthCheck()
|
||||
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring");
|
||||
var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
|
||||
.WithReference(cache)
|
||||
.WithReference(queue)
|
||||
.WithReference(ringService)
|
||||
.WithHttpHealthCheck()
|
||||
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||
.WithReference(ringService);
|
||||
var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
|
||||
.WithReference(cache)
|
||||
.WithReference(queue)
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithHttpHealthCheck()
|
||||
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||
.WithReference(ringService);
|
||||
var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
|
||||
.WithReference(cache)
|
||||
.WithReference(queue)
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithHttpHealthCheck()
|
||||
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||
.WithReference(driveService);
|
||||
var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
|
||||
.WithReference(cache)
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithHttpHealthCheck()
|
||||
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||
.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);
|
||||
|
||||
passService.WithReference(developService).WithReference(driveService);
|
||||
|
||||
List<IResourceBuilder<ProjectResource>> services =
|
||||
[ringService, passService, driveService, sphereService, developService, insightService, zoneService];
|
||||
|
||||
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);
|
||||
|
||||
builder.AddYarp("gateway")
|
||||
.WithHostPort(5000)
|
||||
.WithConfiguration(yarp =>
|
||||
{
|
||||
var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http"));
|
||||
yarp.AddRoute("/ws", ringCluster);
|
||||
yarp.AddRoute("/ring/{**catch-all}", ringCluster)
|
||||
.WithTransformPathRemovePrefix("/ring")
|
||||
.WithTransformPathPrefix("/api");
|
||||
var passCluster = yarp.AddCluster(passService.GetEndpoint("http"));
|
||||
yarp.AddRoute("/.well-known/openid-configuration", passCluster);
|
||||
yarp.AddRoute("/.well-known/jwks", passCluster);
|
||||
yarp.AddRoute("/id/{**catch-all}", passCluster)
|
||||
.WithTransformPathRemovePrefix("/id")
|
||||
.WithTransformPathPrefix("/api");
|
||||
var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http"));
|
||||
yarp.AddRoute("/api/tus", driveCluster);
|
||||
yarp.AddRoute("/drive/{**catch-all}", driveCluster)
|
||||
.WithTransformPathRemovePrefix("/drive")
|
||||
.WithTransformPathPrefix("/api");
|
||||
var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http"));
|
||||
yarp.AddRoute("/sphere/{**catch-all}", sphereCluster)
|
||||
.WithTransformPathRemovePrefix("/sphere")
|
||||
.WithTransformPathPrefix("/api");
|
||||
var developCluster = yarp.AddCluster(developService.GetEndpoint("http"));
|
||||
yarp.AddRoute("/develop/{**catch-all}", developCluster)
|
||||
.WithTransformPathRemovePrefix("/develop")
|
||||
.WithTransformPathPrefix("/api");
|
||||
});
|
||||
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");
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/>
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
|
||||
@@ -12,19 +11,19 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/>
|
||||
<PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" />
|
||||
<PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" />
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.0.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.Docker" Version="13.0.0-preview.1.25560.3"/>
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="13.0.0"/>
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.0"/>
|
||||
</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.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"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17025;http://localhost:15057",
|
||||
"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"
|
||||
"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": {
|
||||
@@ -22,8 +24,9 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185"
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
357
DysonNetwork.Control/aspire-manifest.json
Normal file
357
DysonNetwork.Control/aspire-manifest.json
Normal file
@@ -0,0 +1,357 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop;
|
||||
|
||||
@@ -11,13 +11,13 @@ public class AppDatabase(
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<Developer> Developers { get; set; } = null!;
|
||||
public DbSet<SnDeveloper> Developers { get; set; } = null!;
|
||||
|
||||
public DbSet<DevProject> DevProjects { get; set; } = null!;
|
||||
public DbSet<SnDevProject> DevProjects { get; set; } = null!;
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; } = null!;
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||
public DbSet<BotAccount> BotAccounts { get; set; } = null!;
|
||||
public DbSet<SnCustomApp> CustomApps { get; set; } = null!;
|
||||
public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||
public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -31,10 +31,18 @@ public class AppDatabase(
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ApplyAuditableAndSoftDelete();
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
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:9.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"]
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||
@@ -31,7 +26,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Grpc.Core;
|
||||
@@ -16,10 +16,10 @@ namespace DysonNetwork.Develop.Identity;
|
||||
[Authorize]
|
||||
public class BotAccountController(
|
||||
BotAccountService botService,
|
||||
DeveloperService developerService,
|
||||
DeveloperService ds,
|
||||
DevProjectService projectService,
|
||||
ILogger<BotAccountController> logger,
|
||||
AccountClientHelper accounts,
|
||||
RemoteAccountService remoteAccounts,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
|
||||
)
|
||||
: ControllerBase
|
||||
@@ -50,9 +50,9 @@ public class BotAccountController(
|
||||
]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
[Required][MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
|
||||
[Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty;
|
||||
[Required][MaxLength(1024)] public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(128)] public string Language { get; set; } = "en-us";
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class BotAccountController(
|
||||
|
||||
[MaxLength(256)] public string? Nick { get; set; } = string.Empty;
|
||||
|
||||
[Required] [MaxLength(1024)] public string? Slug { get; set; } = string.Empty;
|
||||
[Required][MaxLength(1024)] public string? Slug { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(128)] public string? Language { get; set; }
|
||||
|
||||
@@ -83,12 +83,12 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
PublisherMemberRole.Viewer))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be an viewer of the developer to list bots");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -108,12 +108,12 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
PublisherMemberRole.Viewer))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be an viewer of the developer to view bot details");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -137,12 +137,12 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
PublisherMemberRole.Editor))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -206,12 +206,12 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
PublisherMemberRole.Editor))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to update a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -222,7 +222,7 @@ public class BotAccountController(
|
||||
if (bot is null || bot.ProjectId != projectId)
|
||||
return NotFound("Bot not found");
|
||||
|
||||
var botAccount = await accounts.GetBotAccount(bot.Id);
|
||||
var botAccount = await remoteAccounts.GetBotAccount(bot.Id);
|
||||
|
||||
if (request.Name is not null) botAccount.Name = request.Name;
|
||||
if (request.Nick is not null) botAccount.Nick = request.Nick;
|
||||
@@ -267,12 +267,12 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
PublisherMemberRole.Editor))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to delete a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -296,7 +296,7 @@ public class BotAccountController(
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/keys")]
|
||||
public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys(
|
||||
public async Task<ActionResult<List<SnApiKey>>> ListBotKeys(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId
|
||||
@@ -305,7 +305,7 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
@@ -314,13 +314,13 @@ public class BotAccountController(
|
||||
{
|
||||
AutomatedId = bot.Id.ToString()
|
||||
});
|
||||
var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList();
|
||||
var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList();
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/keys/{keyId:guid}")]
|
||||
public async Task<ActionResult<ApiKeyReference>> GetBotKey(
|
||||
public async Task<ActionResult<SnApiKey>> GetBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
@@ -329,7 +329,7 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
@@ -338,7 +338,7 @@ public class BotAccountController(
|
||||
{
|
||||
var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
|
||||
if (key == null) return NotFound("API key not found");
|
||||
return Ok(ApiKeyReference.FromProtoValue(key));
|
||||
return Ok(SnApiKey.FromProtoValue(key));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
@@ -353,7 +353,7 @@ public class BotAccountController(
|
||||
}
|
||||
|
||||
[HttpPost("{botId:guid}/keys")]
|
||||
public async Task<ActionResult<ApiKeyReference>> CreateBotKey(
|
||||
public async Task<ActionResult<SnApiKey>> CreateBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
@@ -362,7 +362,7 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
@@ -374,9 +374,9 @@ public class BotAccountController(
|
||||
AccountId = bot.Id.ToString(),
|
||||
Label = request.Label
|
||||
};
|
||||
|
||||
|
||||
var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey);
|
||||
return Ok(ApiKeyReference.FromProtoValue(createdKey));
|
||||
return Ok(SnApiKey.FromProtoValue(createdKey));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||
{
|
||||
@@ -385,7 +385,7 @@ public class BotAccountController(
|
||||
}
|
||||
|
||||
[HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
|
||||
public async Task<ActionResult<ApiKeyReference>> RotateBotKey(
|
||||
public async Task<ActionResult<SnApiKey>> RotateBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
@@ -394,7 +394,7 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
@@ -402,7 +402,7 @@ public class BotAccountController(
|
||||
try
|
||||
{
|
||||
var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
|
||||
return Ok(ApiKeyReference.FromProtoValue(rotatedKey));
|
||||
return Ok(SnApiKey.FromProtoValue(rotatedKey));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
@@ -420,7 +420,7 @@ public class BotAccountController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
@@ -436,17 +436,17 @@ public class BotAccountController(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess(
|
||||
private async Task<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess(
|
||||
string pubName,
|
||||
Guid projectId,
|
||||
Guid botId,
|
||||
Account currentUser,
|
||||
PublisherMemberRole requiredRole)
|
||||
Shared.Proto.PublisherMemberRole requiredRole)
|
||||
{
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer == null) return (null, null, null);
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole))
|
||||
return (null, null, null);
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -457,4 +457,4 @@ public class BotAccountController(
|
||||
|
||||
return (developer, project, bot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
@@ -7,7 +8,7 @@ namespace DysonNetwork.Develop.Identity;
|
||||
public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase
|
||||
{
|
||||
[HttpGet("{botId:guid}")]
|
||||
public async Task<ActionResult<BotAccount>> GetBotTransparentInfo([FromRoute] Guid botId)
|
||||
public async Task<ActionResult<SnBotAccount>> GetBotTransparentInfo([FromRoute] Guid botId)
|
||||
{
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null) return NotFound("Bot not found");
|
||||
@@ -21,7 +22,7 @@ public class BotAccountPublicController(BotAccountService botService, DeveloperS
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/developer")]
|
||||
public async Task<ActionResult<Developer>> GetBotDeveloper([FromRoute] Guid botId)
|
||||
public async Task<ActionResult<SnDeveloper>> GetBotDeveloper([FromRoute] Guid botId)
|
||||
{
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null) return NotFound("Bot not found");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Grpc.Core;
|
||||
@@ -11,25 +10,25 @@ namespace DysonNetwork.Develop.Identity;
|
||||
public class BotAccountService(
|
||||
AppDatabase db,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
|
||||
AccountClientHelper accounts
|
||||
RemoteAccountService remoteAccounts
|
||||
)
|
||||
{
|
||||
public async Task<BotAccount?> GetBotByIdAsync(Guid id)
|
||||
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
|
||||
{
|
||||
return await db.BotAccounts
|
||||
.Include(b => b.Project)
|
||||
.FirstOrDefaultAsync(b => b.Id == id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId)
|
||||
public async Task<List<SnBotAccount>> GetBotsByProjectAsync(Guid projectId)
|
||||
{
|
||||
return await db.BotAccounts
|
||||
.Where(b => b.ProjectId == projectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<BotAccount> CreateBotAsync(
|
||||
DevProject project,
|
||||
public async Task<SnBotAccount> CreateBotAsync(
|
||||
SnDevProject project,
|
||||
string slug,
|
||||
Account account,
|
||||
string? pictureId,
|
||||
@@ -58,7 +57,7 @@ public class BotAccountService(
|
||||
var botAccount = createResponse.Bot;
|
||||
|
||||
// Then create the local bot account
|
||||
var bot = new BotAccount
|
||||
var bot = new SnBotAccount
|
||||
{
|
||||
Id = automatedId,
|
||||
Slug = slug,
|
||||
@@ -89,8 +88,8 @@ public class BotAccountService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BotAccount> UpdateBotAsync(
|
||||
BotAccount bot,
|
||||
public async Task<SnBotAccount> UpdateBotAsync(
|
||||
SnBotAccount bot,
|
||||
Account account,
|
||||
string? pictureId,
|
||||
string? backgroundId
|
||||
@@ -98,7 +97,7 @@ public class BotAccountService(
|
||||
{
|
||||
db.Update(bot);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// Update the bot account in the Pass service
|
||||
@@ -130,7 +129,7 @@ public class BotAccountService(
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async Task DeleteBotAsync(BotAccount bot)
|
||||
public async Task DeleteBotAsync(SnBotAccount bot)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -153,22 +152,21 @@ public class BotAccountService(
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) =>
|
||||
public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) =>
|
||||
(await LoadBotsAccountAsync([bot])).FirstOrDefault();
|
||||
|
||||
public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> bots)
|
||||
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
|
||||
{
|
||||
bots = bots.ToList();
|
||||
var automatedIds = bots.Select(b => b.Id).ToList();
|
||||
var data = await accounts.GetBotAccountBatch(automatedIds);
|
||||
var data = await remoteAccounts.GetBotAccountBatch(automatedIds);
|
||||
|
||||
foreach (var bot in bots)
|
||||
{
|
||||
bot.Account = data
|
||||
.Select(AccountReference.FromProtoValue)
|
||||
.Select(SnAccount.FromProtoValue)
|
||||
.FirstOrDefault(e => e.AutomatedId == bot.Id);
|
||||
}
|
||||
|
||||
return bots as List<BotAccount> ?? [];
|
||||
return bots;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -18,9 +19,9 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
[MaxLength(4096)] string? Description,
|
||||
string? PictureId,
|
||||
string? BackgroundId,
|
||||
CustomAppStatus? Status,
|
||||
CustomAppLinks? Links,
|
||||
CustomAppOauthConfig? OauthConfig
|
||||
Shared.Models.CustomAppStatus? Status,
|
||||
SnCustomAppLinks? Links,
|
||||
SnCustomAppOauthConfig? OauthConfig
|
||||
);
|
||||
|
||||
public record CreateSecretRequest(
|
||||
@@ -50,7 +51,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to list custom apps");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -72,7 +73,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer))
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to list custom apps");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -99,7 +100,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 custom app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -143,7 +144,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 custom app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -180,7 +181,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 custom app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -212,7 +213,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 view app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -250,7 +251,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -263,7 +264,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
|
||||
try
|
||||
{
|
||||
var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret
|
||||
var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret
|
||||
{
|
||||
AppId = appId,
|
||||
Description = request.Description,
|
||||
@@ -309,7 +310,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 view app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -350,7 +351,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -388,7 +389,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
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 rotate app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
@@ -401,7 +402,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
|
||||
try
|
||||
{
|
||||
var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret
|
||||
var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret
|
||||
{
|
||||
Id = secretId,
|
||||
AppId = appId,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
@@ -13,7 +12,7 @@ public class CustomAppService(
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
{
|
||||
public async Task<CustomApp?> CreateAppAsync(
|
||||
public async Task<SnCustomApp?> CreateAppAsync(
|
||||
Guid projectId,
|
||||
CustomAppController.CustomAppRequest request
|
||||
)
|
||||
@@ -25,12 +24,12 @@ public class CustomAppService(
|
||||
if (project == null)
|
||||
return null;
|
||||
|
||||
var app = new CustomApp
|
||||
var app = new SnCustomApp
|
||||
{
|
||||
Slug = request.Slug!,
|
||||
Name = request.Name!,
|
||||
Description = request.Description,
|
||||
Status = request.Status ?? CustomAppStatus.Developing,
|
||||
Status = request.Status ?? Shared.Models.CustomAppStatus.Developing,
|
||||
Links = request.Links,
|
||||
OauthConfig = request.OauthConfig,
|
||||
ProjectId = projectId
|
||||
@@ -46,7 +45,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -65,7 +64,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -84,7 +83,7 @@ public class CustomAppService(
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null)
|
||||
public async Task<SnCustomApp?> GetAppAsync(Guid id, Guid? projectId = null)
|
||||
{
|
||||
var query = db.CustomApps.AsQueryable();
|
||||
|
||||
@@ -96,7 +95,7 @@ public class CustomAppService(
|
||||
return await query.FirstOrDefaultAsync(a => a.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CustomAppSecret>> GetAppSecretsAsync(Guid appId)
|
||||
public async Task<List<SnCustomAppSecret>> GetAppSecretsAsync(Guid appId)
|
||||
{
|
||||
return await db.CustomAppSecrets
|
||||
.Where(s => s.AppId == appId)
|
||||
@@ -104,13 +103,13 @@ public class CustomAppService(
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
|
||||
public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
|
||||
{
|
||||
return await db.CustomAppSecrets
|
||||
.FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
|
||||
}
|
||||
|
||||
public async Task<CustomAppSecret> CreateAppSecretAsync(CustomAppSecret secret)
|
||||
public async Task<SnCustomAppSecret> CreateAppSecretAsync(SnCustomAppSecret secret)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secret.Secret))
|
||||
{
|
||||
@@ -141,7 +140,7 @@ public class CustomAppService(
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate)
|
||||
public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate)
|
||||
{
|
||||
var existingSecret = await db.CustomAppSecrets
|
||||
.FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId);
|
||||
@@ -177,14 +176,14 @@ public class CustomAppService(
|
||||
return res.ToString();
|
||||
}
|
||||
|
||||
public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId)
|
||||
public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId)
|
||||
{
|
||||
return await db.CustomApps
|
||||
.Where(a => a.ProjectId == projectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
|
||||
public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request)
|
||||
{
|
||||
if (request.Slug is not null)
|
||||
app.Slug = request.Slug;
|
||||
@@ -209,7 +208,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -228,7 +227,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -37,7 +38,7 @@ public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppServic
|
||||
if (string.IsNullOrEmpty(request.Secret))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
|
||||
|
||||
IQueryable<CustomAppSecret> q = db.CustomAppSecrets;
|
||||
IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets;
|
||||
switch (request.SecretIdentifierCase)
|
||||
{
|
||||
case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId:
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class Developer
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PublisherId { get; set; }
|
||||
|
||||
[JsonIgnore] public List<DevProject> Projects { get; set; } = [];
|
||||
|
||||
[NotMapped] public PublisherInfo? Publisher { get; set; }
|
||||
}
|
||||
|
||||
public class PublisherInfo
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public PublisherType Type { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Nick { get; set; } = string.Empty;
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public CloudFileReferenceObject? Picture { get; set; }
|
||||
public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
public VerificationMark? Verification { get; set; }
|
||||
public Guid? AccountId { get; set; }
|
||||
public Guid? RealmId { get; set; }
|
||||
|
||||
public static PublisherInfo FromProto(Publisher proto)
|
||||
{
|
||||
var info = new PublisherInfo
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Type = proto.Type == PublisherType.PubIndividual
|
||||
? PublisherType.PubIndividual
|
||||
: PublisherType.PubOrganizational,
|
||||
Name = proto.Name,
|
||||
Nick = proto.Nick,
|
||||
Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio,
|
||||
Verification = proto.VerificationMark is not null
|
||||
? VerificationMark.FromProtoValue(proto.VerificationMark)
|
||||
: null,
|
||||
AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId),
|
||||
RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId)
|
||||
};
|
||||
|
||||
if (proto.Picture != null)
|
||||
{
|
||||
info.Picture = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Picture.Id,
|
||||
Name = proto.Picture.Name,
|
||||
MimeType = proto.Picture.MimeType,
|
||||
Hash = proto.Picture.Hash,
|
||||
Size = proto.Picture.Size
|
||||
};
|
||||
}
|
||||
|
||||
if (proto.Background != null)
|
||||
{
|
||||
info.Background = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Background.Id,
|
||||
Name = proto.Background.Name,
|
||||
MimeType = proto.Background.MimeType,
|
||||
Hash = proto.Background.Hash,
|
||||
Size = (long)proto.Background.Size
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -18,7 +19,7 @@ public class DeveloperController(
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Developer>> GetDeveloper(string name)
|
||||
public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name)
|
||||
{
|
||||
var developer = await ds.GetDeveloperByName(name);
|
||||
if (developer is null) return NotFound();
|
||||
@@ -47,10 +48,9 @@ public class DeveloperController(
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers()
|
||||
public async Task<ActionResult<List<SnDeveloper>>> ListJoinedDevelopers()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
|
||||
var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
|
||||
@@ -69,17 +69,17 @@ public class DeveloperController(
|
||||
|
||||
[HttpPost("{name}/enroll")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "developers.create")]
|
||||
public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name)
|
||||
[AskPermission("developers.create")]
|
||||
public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
PublisherInfo? pub;
|
||||
SnPublisher? pub;
|
||||
try
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||
pub = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
pub = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||
} catch (RpcException ex)
|
||||
{
|
||||
return NotFound(ex.Status.Detail);
|
||||
@@ -90,14 +90,14 @@ public class DeveloperController(
|
||||
{
|
||||
PublisherId = pub.Id.ToString(),
|
||||
AccountId = currentUser.Id,
|
||||
Role = PublisherMemberRole.Owner
|
||||
Role = Shared.Proto.PublisherMemberRole.Owner
|
||||
});
|
||||
if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
|
||||
|
||||
var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
|
||||
if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
|
||||
|
||||
var developer = new Developer
|
||||
var developer = new SnDeveloper
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PublisherId = pub.Id
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,22 +10,22 @@ public class DeveloperService(
|
||||
PublisherService.PublisherServiceClient ps,
|
||||
ILogger<DeveloperService> logger)
|
||||
{
|
||||
public async Task<Developer> LoadDeveloperPublisher(Developer developer)
|
||||
public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer)
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
|
||||
developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||
return developer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers)
|
||||
public async Task<IEnumerable<SnDeveloper>> LoadDeveloperPublisher(IEnumerable<SnDeveloper> developers)
|
||||
{
|
||||
var enumerable = developers.ToList();
|
||||
var pubIds = enumerable.Select(d => d.PublisherId).ToList();
|
||||
var pubRequest = new GetPublisherBatchRequest();
|
||||
pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
|
||||
var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
|
||||
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), PublisherInfo.FromProto);
|
||||
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue);
|
||||
|
||||
return enumerable.Select(d =>
|
||||
{
|
||||
@@ -33,7 +34,7 @@ public class DeveloperService(
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Developer?> GetDeveloperByName(string name)
|
||||
public async Task<SnDeveloper?> GetDeveloperByName(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -50,12 +51,12 @@ public class DeveloperService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Developer?> GetDeveloperById(Guid id)
|
||||
public async Task<SnDeveloper?> GetDeveloperById(Guid id)
|
||||
{
|
||||
return await db.Developers.FirstOrDefaultAsync(d => d.Id == id);
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role)
|
||||
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, Shared.Proto.PublisherMemberRole role)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -56,7 +55,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -66,11 +65,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
@@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
@@ -35,11 +33,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
|
||||
oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
|
||||
links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
|
||||
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
|
||||
oauth_config = table.Column<SnCustomAppOauthConfig>(type: "jsonb", nullable: true),
|
||||
links = table.Column<SnCustomAppLinks>(type: "jsonb", nullable: true),
|
||||
developer_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),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -52,7 +51,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -62,11 +61,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
@@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -77,7 +76,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -94,7 +93,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -104,11 +103,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
@@ -130,7 +129,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -74,7 +73,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -91,7 +90,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -101,11 +100,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
@@ -127,7 +126,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
|
||||
@@ -13,12 +13,16 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
||||
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddAppSwagger();
|
||||
builder.Services.AddDysonAuth();
|
||||
builder.Services.AddPublisherService();
|
||||
builder.Services.AddSphereService();
|
||||
builder.Services.AddAccountService();
|
||||
builder.Services.AddDriveService();
|
||||
|
||||
builder.AddSwaggerManifest(
|
||||
"DysonNetwork.Develop",
|
||||
"The developer portal in the Solar Network."
|
||||
);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
@@ -31,4 +35,6 @@ using (var scope = app.Services.CreateScope())
|
||||
|
||||
app.ConfigureAppMiddleware(builder.Configuration);
|
||||
|
||||
app.UseSwaggerManifest("DysonNetwork.Develop");
|
||||
|
||||
app.Run();
|
||||
@@ -1,21 +1,17 @@
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Develop.Project;
|
||||
|
||||
public class DevProjectService(
|
||||
AppDatabase db,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
public class DevProjectService(AppDatabase db )
|
||||
{
|
||||
public async Task<DevProject> CreateProjectAsync(
|
||||
Developer developer,
|
||||
public async Task<SnDevProject> CreateProjectAsync(
|
||||
SnDeveloper developer,
|
||||
DevProjectController.DevProjectRequest request
|
||||
)
|
||||
{
|
||||
var project = new DevProject
|
||||
var project = new SnDevProject
|
||||
{
|
||||
Slug = request.Slug!,
|
||||
Name = request.Name!,
|
||||
@@ -25,14 +21,14 @@ public class DevProjectService(
|
||||
|
||||
db.DevProjects.Add(project);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
public async Task<DevProject?> GetProjectAsync(Guid id, Guid? developerId = null)
|
||||
public async Task<SnDevProject?> GetProjectAsync(Guid id, Guid? developerId = null)
|
||||
{
|
||||
var query = db.DevProjects.AsQueryable();
|
||||
|
||||
|
||||
if (developerId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.DeveloperId == developerId.Value);
|
||||
@@ -41,14 +37,14 @@ public class DevProjectService(
|
||||
return await query.FirstOrDefaultAsync(p => p.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<DevProject>> GetProjectsByDeveloperAsync(Guid developerId)
|
||||
public async Task<List<SnDevProject>> GetProjectsByDeveloperAsync(Guid developerId)
|
||||
{
|
||||
return await db.DevProjects
|
||||
.Where(p => p.DeveloperId == developerId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<DevProject?> UpdateProjectAsync(
|
||||
public async Task<SnDevProject?> UpdateProjectAsync(
|
||||
Guid id,
|
||||
Guid developerId,
|
||||
DevProjectController.DevProjectRequest request
|
||||
@@ -74,4 +70,4 @@ public class DevProjectService(
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5156",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -14,7 +13,6 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7192;http://localhost:5156",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Net;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Prometheus;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
@@ -11,23 +8,20 @@ public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapMetrics();
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
|
||||
app.ConfigureForwardedHeaders(configuration);
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
app.UseMiddleware<RemotePermissionMiddleware>();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGrpcService<CustomAppServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
@@ -7,7 +6,6 @@ using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
@@ -18,9 +16,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddLocalization();
|
||||
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
@@ -34,6 +30,7 @@ public static class ServiceCollectionExtensions
|
||||
});
|
||||
|
||||
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
@@ -57,23 +54,7 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "Develop API",
|
||||
});
|
||||
});
|
||||
services.AddOpenApi();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,13 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;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"
|
||||
},
|
||||
"KnownProxies": ["127.0.0.1", "::1"],
|
||||
"Swagger": {
|
||||
"PublicBasePath": "/develop"
|
||||
},
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
],
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
},
|
||||
"Service": {
|
||||
"Name": "DysonNetwork.Develop",
|
||||
"Url": "https://localhost:7192"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using DysonNetwork.Drive.Billing;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Drive.Storage.Model;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using TaskStatus = DysonNetwork.Drive.Storage.Model.TaskStatus;
|
||||
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
@@ -17,12 +18,16 @@ public class AppDatabase(
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<FilePool> Pools { get; set; } = null!;
|
||||
public DbSet<FileBundle> Bundles { get; set; } = null!;
|
||||
public DbSet<SnFileBundle> Bundles { get; set; } = null!;
|
||||
|
||||
public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
|
||||
|
||||
public DbSet<CloudFile> Files { get; set; } = null!;
|
||||
public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
|
||||
public DbSet<SnCloudFile> Files { get; set; } = null!;
|
||||
public DbSet<SnCloudFileReference> FileReferences { get; set; } = null!;
|
||||
public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
|
||||
|
||||
public DbSet<PersistentTask> Tasks { get; set; } = null!;
|
||||
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -40,52 +45,12 @@ public class AppDatabase(
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
|
||||
var method = typeof(AppDatabase)
|
||||
.GetMethod(nameof(SetSoftDeleteFilter),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(entityType.ClrType);
|
||||
|
||||
method.Invoke(null, [modelBuilder]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
|
||||
where TEntity : ModelBase
|
||||
{
|
||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
||||
{
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entry.Entity.CreatedAt = now;
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Modified:
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Deleted:
|
||||
entry.State = EntityState.Modified;
|
||||
entry.Entity.DeletedAt = now;
|
||||
break;
|
||||
case EntityState.Detached:
|
||||
case EntityState.Unchanged:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.ApplyAuditableAndSoftDelete();
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +102,45 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
|
||||
}
|
||||
}
|
||||
|
||||
public class PersistentTaskCleanupJob(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<PersistentTaskCleanupJob> logger
|
||||
) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
logger.LogInformation("Cleaning up stale persistent tasks...");
|
||||
|
||||
// Get the PersistentTaskService from DI
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var persistentTaskService = scope.ServiceProvider.GetService(typeof(PersistentTaskService));
|
||||
|
||||
if (persistentTaskService is PersistentTaskService service)
|
||||
{
|
||||
// Clean up tasks for all users (you might want to add user-specific logic here)
|
||||
// For now, we'll clean up tasks older than 30 days for all users
|
||||
var cutoff = SystemClock.Instance.GetCurrentInstant() - Duration.FromDays(30);
|
||||
var tasksToClean = await service.GetUserTasksAsync(
|
||||
Guid.Empty, // This would need to be adjusted for multi-user cleanup
|
||||
status: TaskStatus.Completed | TaskStatus.Failed | TaskStatus.Cancelled | TaskStatus.Expired
|
||||
);
|
||||
|
||||
var cleanedCount = 0;
|
||||
foreach (var task in tasksToClean.Items.Where(t => t.UpdatedAt < cutoff))
|
||||
{
|
||||
await service.CancelTaskAsync(task.TaskId); // Or implement a proper cleanup method
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
logger.LogInformation("Cleaned up {Count} stale persistent tasks", cleanedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("PersistentTaskService not found in DI container");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||
{
|
||||
public AppDatabase CreateDbContext(string[] args)
|
||||
@@ -150,35 +154,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
public static class OptionalQueryExtensions
|
||||
{
|
||||
public static IQueryable<T> If<T>(
|
||||
this IQueryable<T> source,
|
||||
bool condition,
|
||||
Func<IQueryable<T>, IQueryable<T>> transform
|
||||
)
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<T, TP>(
|
||||
this IIncludableQueryable<T, TP> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<T, TP>(
|
||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Billing;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class QuotaService(
|
||||
|
||||
var (based, extra) = await GetQuotaVerbose(accountId);
|
||||
var quota = based + extra;
|
||||
await cache.SetAsync(cacheKey, quota);
|
||||
await cache.SetAsync(cacheKey, quota, expiry: TimeSpan.FromMinutes(30));
|
||||
return quota;
|
||||
}
|
||||
|
||||
|
||||
1
DysonNetwork.Drive/Client/.gitattributes
vendored
1
DysonNetwork.Drive/Client/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
31
DysonNetwork.Drive/Client/.gitignore
vendored
31
DysonNetwork.Drive/Client/.gitignore
vendored
@@ -1,31 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
**/node_modules/highlight.js/
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"oxc.oxc-vscode",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
@@ -1,955 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@solar-network/pass",
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@fontsource-variable/nunito": "^5.2.6",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"aspnet-prerendering": "^3.0.1",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"vue": "^3.5.17",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.4",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-oxlint": "~1.1.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"jiti": "^2.4.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vue-tsc": "^2.2.12",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||
|
||||
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
|
||||
|
||||
"@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="],
|
||||
|
||||
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="],
|
||||
|
||||
"@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="],
|
||||
|
||||
"@css-render/plugin-bem": ["@css-render/plugin-bem@0.15.14", "", { "peerDependencies": { "css-render": "~0.15.14" } }, "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg=="],
|
||||
|
||||
"@css-render/vue3-ssr": ["@css-render/vue3-ssr@0.15.14", "", { "peerDependencies": { "vue": "^3.0.11" } }, "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" } }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
|
||||
|
||||
"@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="],
|
||||
|
||||
"@fingerprintjs/fingerprintjs": ["@fingerprintjs/fingerprintjs@4.6.2", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw=="],
|
||||
|
||||
"@fontsource-variable/nunito": ["@fontsource-variable/nunito@5.2.6", "", {}, "sha512-dGYTQ0Hl94jjfMraYefrURHGH8fk/vL/1zYAZGofiPJVs6C0OkM8T87Te5Gwrbe6HG/XEMm5lib8AqasTN3ucw=="],
|
||||
|
||||
"@hcaptcha/vue3-hcaptcha": ["@hcaptcha/vue3-hcaptcha@1.3.0", "", { "dependencies": { "vue": "^3.2.19" } }, "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.77.0", "", {}, "sha512-cMbHs/DaomWSjxeJ79G10GA5hzJW9A7CZ+/cO+KuPZ7Trf3Rr07qSLauC4Ns8ba4DKVDjd8VSC9nVLpw6jpoGQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.77.0", "", {}, "sha512-iUQj185VvCPnSba+ltUV5tVDrPX6LeZVtQywnnoGbe4oJ1VKvDKisjGkD/AvVtdm98b/BdsVS35IlJV1m2mBBA=="],
|
||||
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sSnR3SOxIU/QfaqXrcQ0UVUkzJO0bcInQ7dMhHa102gVAgWjp1fBeMVCM0adEY0UNmEXrRkgD/rQtQgn9YAU+w=="],
|
||||
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvd3fHnzY2OYbmsg9NSGPoBkGViDGHSFnBKyJQ9LOIw7lxAyQBG2Quxc3GYPFR/f9OYho9C3p4+dIaAJfKhnsw=="],
|
||||
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MgW4iskOdXuoR+wDXIJUfbdnTg2eo2FnQRaD6ZqhnDTDa7LnV+06rp/Cg3aGj2X9jSEcKDv/bMbYQuot7WRs6Q=="],
|
||||
|
||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+pkEKmDRdrW+y0gtZ/m68ElVW2VZgATGbMxDgDYFpdiMx9Y0pUPwTMZ2EX/17Aslop4c1BiDSFDK7aEBxKR2g=="],
|
||||
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wNBsXCKVZMvUTcFitrV1wTsdhUAv8l+XQxHxciZ2SO6dpNnWEb2YCxSAIOXeyzBLdO4pIODYcSy38CvGue7TwA=="],
|
||||
|
||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pZD0lt6A5j2Wp70fgIYk4GoPfKTZ8mHWamWIpKFT7aSkFkiOi6nhLWDFvMEIHWRTK3LgkWUNcnWPp4brvin4wQ=="],
|
||||
|
||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rT6uXQvE80+B+L04HJf30uF26426FPI9i9DAY2AxBUhrpNwhqkDEhQdd9ilFWVC7SSbpHgAs50lo+ImSAAkHPQ=="],
|
||||
|
||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x6r5yvM3wEty93Bx0NuNK+kutUyS/K55itkUrxdExoK6GcmVDboGGuhju9HyU2cM/IWLEWO8RHcXSyaxr9GR5g=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.2.7", "", {}, "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.27", "", { "os": "android", "cpu": "arm64" }, "sha512-IJL3efUJmvb5MfTEi7bGK4jq3ZFAzVbSy+vmul0DcdrglUd81Tfyy7Zzq2oM0tUgmACG32d8Jz/ykbpbf+3C5A=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TXTiuHbtnHfb0c44vNfWfIyEFJ0BFUf63ip9Z4mj8T2zRcZXQYVger4OuAxnwGNGBgDyHo1VaNBG+Vxn2VrpqQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jpjflgvbolh+fAaaEajPJQCOpZMawYMbNVzuZp3nidX1B7kMAP7NEKp9CWzthoL2Y8RfD7OApN6bx4+vFurTaw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.27", "", { "os": "freebsd", "cpu": "x64" }, "sha512-07ZNlXIunyS1jCTnene7aokkzCZNBUnmnJWu4Nz5X5XQvVHJNjsDhPFJTlNmneSDzA3vGkRNwdECKXiDTH/CqA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm" }, "sha512-z74ah00oyKnTUtaIbg34TaIU1PYM8tGE1bK6aUs8OLZ9sWW4g3Xo5A0nit2zyeanmYFvrAUxnt3Bpk+mTZCtlg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-b9oKl/M5OIyAcosS73BmjOZOjvcONV97t2SnKpgwfDX/mjQO3dBgTYyvHMFA6hfhIDW1+2XVQR/k5uzBULFhoA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-RmaNSkVmAH8u/r5Q+v4O0zL4HY8pLrvlM5wBoBrb/QHDQgksGKBqhecpg1ERER0Q7gMh/GJUz6JiiD55Q+9UOA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-ohos": ["@rolldown/binding-linux-arm64-ohos@1.0.0-beta.27", "", { "os": "none", "cpu": "arm64" }, "sha512-gq78fI/g0cp1UKFMk53kP/oZAgYOXbaqdadVMuCJc0CoSkDJcpO2YIasRs/QYlE91QWfcHD5RZl9zbf4ksTS/w=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-yS/GreJ6BT44dHu1WLigc50S8jZA+pDzzsf8tqRptUTwi5YW7dX3NqcDlc/lXsZqu57aKynLljgClYAm90LEKw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-6FV9To1sXewGHY4NaCPeOE5p5o1qfuAjj+m75WVIPw9HEJVsQoC5QiTL5wWVNqSMch4X0eWnQ6WsQolU6sGMIA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.27", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.12" }, "cpu": "none" }, "sha512-VcxdhF0PQda9krFJHw4DqUkdAsHWYs/Uz/Kr/zhU8zMFDzmK6OdUgl9emGj9wTzXAEHYkAMDhk+OJBRJvp424g=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "arm64" }, "sha512-3bXSARqSf8jLHrQ1/tw9pX1GwIR9jA6OEsqTgdC0DdpoZ+34sbJXE9Nse3dQ0foGLKBkh4PqDv/rm2Thu9oVBw=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "ia32" }, "sha512-xPGcKb+W8NIWAf5KApsUIrhiKH5NImTarICge5jQ2m0BBxD31crio4OXy/eYVq5CZkqkqszLQz2fWZcWNmbzlQ=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "x64" }, "sha512-3y1G8ARpXBAcz4RJM5nzMU6isS/gXZl8SuX8lS2piFOnQMiOp6ajeelnciD+EgG4ej793zvNvr+WZtdnao2yrw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||
|
||||
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.16.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.37.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.37.0", "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.37.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.37.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.37.0", "@typescript-eslint/tsconfig-utils": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w=="],
|
||||
|
||||
"@vicons/material": ["@vicons/material@0.13.0", "", {}, "sha512-lKVxFNprM+CaBkUH3gt6VjIeiMsKQl2zARQMwTCZruQl2vRHzyeZiKeCflWS99CEfv2JzX/6y697smxlzyxcVw=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.19" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.0.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/plugin-transform-typescript": "^7.27.1", "@rolldown/pluginutils": "^1.0.0-beta.21", "@vue/babel-plugin-jsx": "^1.4.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, "sha512-X7qmQMXbdDh+sfHUttXokPD0cjPkMFoae7SgbkF9vi3idGUKmxLcnU2Ug49FHwiKXebfzQRIm5yK3sfCJzNBbg=="],
|
||||
|
||||
"@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="],
|
||||
|
||||
"@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="],
|
||||
|
||||
"@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="],
|
||||
|
||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.4.0", "", {}, "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw=="],
|
||||
|
||||
"@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.4.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "@vue/babel-helper-vue-transform-on": "1.4.0", "@vue/babel-plugin-resolve-type": "1.4.0", "@vue/shared": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.4.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/parser": "^7.26.9", "@vue/compiler-sfc": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.17", "", { "dependencies": { "@vue/compiler-core": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/compiler-core": "3.5.17", "@vue/compiler-dom": "3.5.17", "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="],
|
||||
|
||||
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
|
||||
|
||||
"@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="],
|
||||
|
||||
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.7", "", { "dependencies": { "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA=="],
|
||||
|
||||
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.7", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw=="],
|
||||
|
||||
"@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@10.2.0", "", { "dependencies": { "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw=="],
|
||||
|
||||
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.6.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.35.1", "fast-glob": "^3.3.3", "typescript-eslint": "^8.35.1", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ=="],
|
||||
|
||||
"@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.17", "", { "dependencies": { "@vue/shared": "3.5.17" } }, "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/runtime-core": "3.5.17", "@vue/shared": "3.5.17", "csstype": "^3.1.3" } }, "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.17", "", { "dependencies": { "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "vue": "3.5.17" } }, "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="],
|
||||
|
||||
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aspnet-prerendering": ["aspnet-prerendering@3.0.1", "", { "dependencies": { "domain-task": "^3.0.0" } }, "sha512-nfOQYVKW3sYQMZBXNM2KPrXU2MOBuLn/gszRZM0Y1Pj4EpzCw1KjXiO681eQo4ZR1TLLzJ8L2sQbq0qeC1zxVg=="],
|
||||
|
||||
"async-validator": ["async-validator@4.2.5", "", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||
|
||||
"cfturnstile-vue3": ["cfturnstile-vue3@2.0.0", "", { "dependencies": { "vue": "^3.2.38" } }, "sha512-wamRC8ZoUAjvfOVoPAbJM14qqxc0gfjqfV6ESZh4rMs7G0yp+R4dpHNjxa7YAjdFTutaviMEZYCuK9tM4ZaGJQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combine-errors": ["combine-errors@3.0.3", "", { "dependencies": { "custom-error-instance": "2.1.1", "lodash.uniqby": "4.5.0" } }, "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-render": ["css-render@0.15.14", "", { "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" } }, "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"custom-error-instance": ["custom-error-instance@2.1.1", "", {}, "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="],
|
||||
|
||||
"date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
|
||||
|
||||
"date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="],
|
||||
|
||||
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"domain-context": ["domain-context@0.5.1", "", {}, "sha512-WyTWkXciNvYYaQzdnKJtjlVSXHivtt0E/vCv36Bkwh+Sk4NXkrQpHxZT5BHYmKRVgxWMol1wcdurZCzyTT6Euw=="],
|
||||
|
||||
"domain-task": ["domain-task@3.0.3", "", { "dependencies": { "domain-context": "^0.5.1", "is-absolute-url": "^2.1.0", "isomorphic-fetch": "^2.2.1" } }, "sha512-7oAiY1AvjhVNVJbOwSHbrm6lEHczOSSCSqDkHp2ZO7vb/iOCGl7YNk/1cv4yKwSGhBMpBZ5mu+7cMorbWxWvOg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.183", "", {}, "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA=="],
|
||||
|
||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="],
|
||||
|
||||
"eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="],
|
||||
|
||||
"eslint-plugin-oxlint": ["eslint-plugin-oxlint@1.1.0", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-spDWxcsAfoUDjSwxPrP2gfuOJ2Hrv8faqQ5Vkm90lURp4no5aWJQ09xRKmZroIPTuQCKYgG9nvnakdIbXGlijg=="],
|
||||
|
||||
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw=="],
|
||||
|
||||
"eslint-plugin-vue": ["eslint-plugin-vue@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" } }, "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"evtd": ["evtd@0.2.4", "", {}, "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw=="],
|
||||
|
||||
"execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-absolute-url": ["is-absolute-url@2.1.0", "", {}, "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"isomorphic-fetch": ["isomorphic-fetch@2.2.1", "", { "dependencies": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" } }, "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||
|
||||
"lodash._baseiteratee": ["lodash._baseiteratee@4.7.0", "", { "dependencies": { "lodash._stringtopath": "~4.8.0" } }, "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ=="],
|
||||
|
||||
"lodash._basetostring": ["lodash._basetostring@4.12.0", "", {}, "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw=="],
|
||||
|
||||
"lodash._baseuniq": ["lodash._baseuniq@4.6.0", "", { "dependencies": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" } }, "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A=="],
|
||||
|
||||
"lodash._createset": ["lodash._createset@4.0.3", "", {}, "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA=="],
|
||||
|
||||
"lodash._root": ["lodash._root@3.0.1", "", {}, "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ=="],
|
||||
|
||||
"lodash._stringtopath": ["lodash._stringtopath@4.8.0", "", { "dependencies": { "lodash._basetostring": "~4.12.0" } }, "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
|
||||
|
||||
"lodash.uniqby": ["lodash.uniqby@4.5.0", "", { "dependencies": { "lodash._baseiteratee": "~4.7.0", "lodash._baseuniq": "~4.6.0" } }, "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"naive-ui": ["naive-ui@2.42.0", "", { "dependencies": { "@css-render/plugin-bem": "^0.15.14", "@css-render/vue3-ssr": "^0.15.14", "@types/katex": "^0.16.2", "@types/lodash": "^4.14.198", "@types/lodash-es": "^4.17.9", "async-validator": "^4.2.5", "css-render": "^0.15.14", "csstype": "^3.1.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", "evtd": "^0.2.4", "highlight.js": "^11.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "seemly": "^0.3.8", "treemate": "^0.3.11", "vdirs": "^0.1.8", "vooks": "^0.2.12", "vueuc": "^0.4.63" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-c7cXR2YgOjgtBadXHwiWL4Y0tpGLAI5W5QzzHksOi22iuHXoSGMAzdkVTGVPE/PM0MSGQ/JtUIzCx2Y0hU0vTQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-fetch": ["node-fetch@1.7.3", "", { "dependencies": { "encoding": "^0.1.11", "is-stream": "^1.0.1" } }, "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="],
|
||||
|
||||
"npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"oxlint": ["oxlint@1.1.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.1.0", "@oxlint/darwin-x64": "1.1.0", "@oxlint/linux-arm64-gnu": "1.1.0", "@oxlint/linux-arm64-musl": "1.1.0", "@oxlint/linux-x64-gnu": "1.1.0", "@oxlint/linux-x64-musl": "1.1.0", "@oxlint/win32-arm64": "1.1.0", "@oxlint/win32-x64": "1.1.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-OVNpaoaQCUHHhCv5sYMPJ7Ts5k7ziw0QteH1gBSwF3elf/8GAew2Uh/0S7HsU1iGtjhlFy80+A8nwIb3Tq6m1w=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
||||
|
||||
"pinia": ["pinia@3.0.3", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="],
|
||||
|
||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
|
||||
|
||||
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.27", "", { "dependencies": { "@oxc-project/runtime": "=0.77.0", "@oxc-project/types": "=0.77.0", "@rolldown/pluginutils": "1.0.0-beta.27", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-x64": "1.0.0-beta.27", "@rolldown/binding-freebsd-x64": "1.0.0-beta.27", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.27", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.27", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.27", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.27" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-aYiJmzKoUHoaaEZLRegYVfZkXW7gzdgSbq+u5cXQ6iXc/y8tnQ3zGffQo44Pr1lTKeLluw3bDIDUCx/NAzqKeA=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"seemly": ["seemly@0.3.10", "", {}, "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"synckit": ["synckit@0.11.8", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"treemate": ["treemate@0.3.11", "", {}, "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tus-js-client": ["tus-js-client@4.3.1", "", { "dependencies": { "buffer-from": "^1.1.2", "combine-errors": "^3.0.3", "is-stream": "^2.0.0", "js-base64": "^3.7.2", "lodash.throttle": "^4.1.1", "proper-lockfile": "^4.1.2", "url-parse": "^1.5.7" } }, "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vdirs": ["vdirs@0.1.8", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw=="],
|
||||
|
||||
"vite": ["rolldown-vite@7.0.9", "", { "dependencies": { "fdir": "^6.4.6", "lightningcss": "^1.30.1", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.27", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RxVP6CY9CNCEM9UecdytqeADxOGSjgkfSE/eI986sM7I3/F09lQ9UfQo3y6W10ICBppKsEHe71NbCX/tirYDFg=="],
|
||||
|
||||
"vite-hot-client": ["vite-hot-client@2.1.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ=="],
|
||||
|
||||
"vite-plugin-inspect": ["vite-plugin-inspect@0.8.9", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" } }, "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A=="],
|
||||
|
||||
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@7.7.7", "", { "dependencies": { "@vue/devtools-core": "^7.7.7", "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "execa": "^9.5.2", "sirv": "^3.0.1", "vite-plugin-inspect": "0.8.9", "vite-plugin-vue-inspector": "^5.3.1" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ=="],
|
||||
|
||||
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="],
|
||||
|
||||
"vooks": ["vooks@0.2.12", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q=="],
|
||||
|
||||
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||
|
||||
"vue": ["vue@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/compiler-sfc": "3.5.17", "@vue/runtime-dom": "3.5.17", "@vue/server-renderer": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g=="],
|
||||
|
||||
"vue-chartjs": ["vue-chartjs@5.3.2", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw=="],
|
||||
|
||||
"vue-eslint-parser": ["vue-eslint-parser@10.2.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw=="],
|
||||
|
||||
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
|
||||
|
||||
"vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="],
|
||||
|
||||
"vueuc": ["vueuc@0.4.64", "", { "dependencies": { "@css-render/vue3-ssr": "^0.15.10", "@juggle/resize-observer": "^3.3.1", "css-render": "^0.15.10", "evtd": "^0.2.4", "seemly": "^0.3.6", "vdirs": "^0.1.4", "vooks": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="],
|
||||
|
||||
"@vue/devtools-core/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
||||
|
||||
"@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"css-render/csstype": ["csstype@3.0.11", "", {}, "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"node-fetch/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
}
|
||||
}
|
||||
1
DysonNetwork.Drive/Client/env.d.ts
vendored
1
DysonNetwork.Drive/Client/env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,31 +0,0 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
...pluginOxlint.configs['flat/recommended'],
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
},
|
||||
skipFormatting,
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Solar Network Drive</title>
|
||||
<app-data />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "@solar-network/drive",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||
"lint:eslint": "eslint . --fix",
|
||||
"lint": "run-s lint:*",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@fontsource-variable/nunito": "^5.2.6",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"aspnet-prerendering": "^3.0.1",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"vue": "^3.5.17",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.4",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-oxlint": "~1.1.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"jiti": "^2.4.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vue-tsc": "^2.2.12"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
font-family: 'Nunito Variable', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<n-select
|
||||
v-model:value="selectedBundle"
|
||||
:options="options"
|
||||
placeholder="Select a bundle"
|
||||
@update:value="handleBundleChange"
|
||||
filterable
|
||||
remote
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSelect } from 'naive-ui'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['update:bundle'])
|
||||
|
||||
const selectedBundle = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const options = ref<any[]>([])
|
||||
|
||||
async function fetchBundles(term: string | null = null) {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await fetch(`/api/bundles/me?${term ? `term=${term}` : ''}`)
|
||||
const data = await resp.json()
|
||||
options.value = data.map((bundle: any) => ({
|
||||
label: bundle.name,
|
||||
value: bundle.id,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bundles:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(query: string) {
|
||||
fetchBundles(query)
|
||||
}
|
||||
|
||||
function handleBundleChange(value: string) {
|
||||
emit('update:bundle', value)
|
||||
}
|
||||
|
||||
onMounted(() => fetchBundles())
|
||||
</script>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<n-select
|
||||
:value="modelValue"
|
||||
@update:value="onUpdate"
|
||||
:options="pools ?? []"
|
||||
:render-label="renderPoolSelectLabel"
|
||||
:render-tag="renderSingleSelectTag"
|
||||
value-field="id"
|
||||
label-field="name"
|
||||
:placeholder="props.placeholder || 'Select a file pool to upload'"
|
||||
:size="props.size || 'large'"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NSelect,
|
||||
NTag,
|
||||
NDivider,
|
||||
NTooltip,
|
||||
type SelectOption,
|
||||
type SelectRenderTag,
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, ref, watch } from 'vue'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
import { formatBytes } from '@/views/format'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null
|
||||
placeholder?: string | undefined
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large' | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:pool'])
|
||||
|
||||
type SnFilePoolOption = SnFilePool & any
|
||||
|
||||
const pools = ref<SnFilePoolOption[] | undefined>()
|
||||
async function fetchPools() {
|
||||
const resp = await fetch('/api/pools')
|
||||
pools.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchPools())
|
||||
|
||||
function onUpdate(value: string | null) {
|
||||
emit('update:modelValue', value)
|
||||
if (value === null) {
|
||||
emit('update:pool', null)
|
||||
return
|
||||
}
|
||||
if (pools.value) {
|
||||
const pool = pools.value.find((p) => p.id === value) ?? null
|
||||
emit('update:pool', pool)
|
||||
}
|
||||
}
|
||||
|
||||
watch(pools, (newPools) => {
|
||||
if (props.modelValue && newPools) {
|
||||
const pool = newPools.find((p) => p.id === props.modelValue) ?? null
|
||||
emit('update:pool', pool)
|
||||
}
|
||||
})
|
||||
|
||||
const renderSingleSelectTag: SelectRenderTag = ({ option }) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
[option.name as string],
|
||||
)
|
||||
}
|
||||
|
||||
const perkPrivilegeList = ['Stellar', 'Nova', 'Supernova']
|
||||
|
||||
function renderPoolSelectLabel(option: SelectOption & SnFilePool) {
|
||||
const policy: any = option.policy_config
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
padding: '8px 2px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('div', null, [option.name as string]),
|
||||
option.description &&
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
fontSize: '0.875rem',
|
||||
opacity: '0.75',
|
||||
},
|
||||
},
|
||||
option.description,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
marginBottom: '4px',
|
||||
fontSize: '0.75rem',
|
||||
opacity: '0.75',
|
||||
},
|
||||
},
|
||||
[
|
||||
policy.max_file_size && h('span', `Max ${formatBytes(policy.max_file_size)}`),
|
||||
policy.accept_types &&
|
||||
h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
trigger: () => h('span', `Accept limited types`),
|
||||
default: () => h('span', policy.accept_types.join(', ')),
|
||||
},
|
||||
),
|
||||
policy.require_privilege &&
|
||||
h('span', `Require ${perkPrivilegeList[policy.require_privilege - 1]} Program`),
|
||||
h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)}`),
|
||||
]
|
||||
.filter((el) => el)
|
||||
.flatMap((el, idx, arr) =>
|
||||
idx < arr.length - 1 ? [el, h(NDivider, { vertical: true })] : [el],
|
||||
),
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
marginTop: '2px',
|
||||
marginLeft: '-2px',
|
||||
marginRight: '-2px',
|
||||
},
|
||||
},
|
||||
[
|
||||
policy.public_usable &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Public Shared' },
|
||||
),
|
||||
policy.public_indexable &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'success',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Public Indexable' },
|
||||
),
|
||||
policy.allow_encryption &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'warning',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Allow Encryption' },
|
||||
),
|
||||
policy.allow_anonymous &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Allow Anonymous' },
|
||||
),
|
||||
policy.enable_recycle &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Recycle Enabled' },
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-collapse-transition :show="showRecycleHint">
|
||||
<n-alert size="small" type="warning" title="Recycle Enabled" class="mb-3">
|
||||
You're uploading to a pool which enabled recycle. If the file you uploaded didn't referenced
|
||||
from the Solar Network. It will be marked and will be deleted some while later.
|
||||
</n-alert>
|
||||
</n-collapse-transition>
|
||||
|
||||
<n-collapse-transition :show="modeAdvanced">
|
||||
<n-card title="Advance Options" size="small" class="mb-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Password</p>
|
||||
<n-input
|
||||
v-model:value="filePass"
|
||||
:disabled="!currentFilePool?.allow_encryption"
|
||||
placeholder="Enter password to protect the file"
|
||||
show-password-toggle
|
||||
size="large"
|
||||
type="password"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="pl-1 text-xs opacity-75 mt-[-4px]">
|
||||
Only available for Stellar Program and certian file pool.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Expiration Date</p>
|
||||
<n-date-picker
|
||||
v-model:value="fileExpire"
|
||||
type="datetime"
|
||||
clearable
|
||||
:is-date-disabled="disablePreviousDate"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentFilePool?.policy_config?.enable_fast_upload || route.query.pool"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p class="pl-1 mb-0.5">Fast Upload</p>
|
||||
<n-switch v-model:value="fastUpload" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-collapse-transition>
|
||||
|
||||
<n-upload
|
||||
multiple
|
||||
directory-dnd
|
||||
with-credentials
|
||||
show-preview-button
|
||||
list-type="image"
|
||||
show-download-button
|
||||
:custom-request="customRequest"
|
||||
:custom-download="customDownload"
|
||||
:create-thumbnail-url="createThumbnailUrl"
|
||||
@preview="customPreview"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3">
|
||||
<cloud-upload-round />
|
||||
</n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">
|
||||
Strictly prohibit from uploading sensitive information. For example, your bank card PIN or
|
||||
your credit card expiry date.
|
||||
</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NIcon,
|
||||
NText,
|
||||
NP,
|
||||
NInput,
|
||||
NCollapseTransition,
|
||||
NDatePicker,
|
||||
NAlert,
|
||||
NCard,
|
||||
NSwitch,
|
||||
type UploadCustomRequestOptions,
|
||||
type UploadSettledFileInfo,
|
||||
type UploadFileInfo,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { CloudUploadRound } from '@vicons/material'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
|
||||
import * as tus from 'tus-js-client'
|
||||
|
||||
const props = defineProps<{
|
||||
filePool: string | null
|
||||
modeAdvanced: boolean
|
||||
pools: SnFilePool[]
|
||||
bundleId?: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const filePass = ref<string>('')
|
||||
const fileExpire = ref<number | null>(null)
|
||||
const fastUpload = ref<boolean>(false)
|
||||
|
||||
const effectiveFilePool = computed(() => (route.query.pool as string) || props.filePool)
|
||||
|
||||
const currentFilePool = computed(() => {
|
||||
if (!effectiveFilePool.value) return null
|
||||
return props.pools?.find((pool) => pool.id === effectiveFilePool.value) ?? null
|
||||
})
|
||||
const showRecycleHint = computed(() => {
|
||||
if (!effectiveFilePool.value) return true
|
||||
return currentFilePool.value?.policy_config?.enable_recycle || false
|
||||
})
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
async function customRequest({
|
||||
file,
|
||||
headers,
|
||||
withCredentials,
|
||||
onFinish,
|
||||
onError,
|
||||
onProgress,
|
||||
}: UploadCustomRequestOptions) {
|
||||
if (fastUpload.value) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', await file.file!.arrayBuffer())
|
||||
const hashString = Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const resp = await fetch('/api/files/fast', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: file.name,
|
||||
size: file.file?.size,
|
||||
hash: hashString,
|
||||
mime_type: file.file?.type,
|
||||
pool_id: effectiveFilePool.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
messageDisplay.error(`Failed to get presigned URL: ${await resp.text()}`)
|
||||
onError()
|
||||
return
|
||||
}
|
||||
|
||||
const respData = await resp.json()
|
||||
const url = respData.fast_upload_link
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url, true)
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
onProgress({ percent: (event.loaded / event.total) * 100 })
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
onFinish()
|
||||
} else {
|
||||
messageDisplay.error(`Upload failed: ${xhr.responseText}`)
|
||||
onError()
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
messageDisplay.error('Upload failed due to a network error.')
|
||||
onError()
|
||||
}
|
||||
xhr.send(file.file)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
messageDisplay.error(`Upload failed: ${e}`)
|
||||
onError()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const requestHeaders: Record<string, string> = {}
|
||||
if (effectiveFilePool.value) requestHeaders['X-FilePool'] = effectiveFilePool.value
|
||||
if (filePass.value) requestHeaders['X-FilePass'] = filePass.value
|
||||
if (fileExpire.value) requestHeaders['X-FileExpire'] = fileExpire.value.toString()
|
||||
if (props.bundleId) requestHeaders['X-FileBundle'] = props.bundleId
|
||||
const upload = new tus.Upload(file.file as any, {
|
||||
endpoint: '/api/tus',
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
removeFingerprintOnSuccess: false,
|
||||
uploadDataDuringCreation: false,
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
'content-type': file.type ?? 'application/octet-stream',
|
||||
},
|
||||
headers: {
|
||||
'X-DirectUpload': 'true',
|
||||
...requestHeaders,
|
||||
...headers,
|
||||
},
|
||||
onShouldRetry: () => false,
|
||||
onError: function (error) {
|
||||
if (error instanceof tus.DetailedError) {
|
||||
const failedBody = error.originalResponse?.getBody()
|
||||
if (failedBody != null)
|
||||
messageDisplay.error(`Upload failed: ${failedBody}`, {
|
||||
duration: 10000,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
console.error('[DRIVE] Upload failed:', error)
|
||||
onError()
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
onProgress({ percent: (bytesUploaded / bytesTotal) * 100 })
|
||||
},
|
||||
onSuccess: function (payload) {
|
||||
const rawInfo = payload.lastResponse.getHeader('x-fileinfo')
|
||||
const jsonInfo = JSON.parse(rawInfo as string)
|
||||
console.log('[DRIVE] Upload successful: ', jsonInfo)
|
||||
file.url = `/api/files/${jsonInfo.id}`
|
||||
file.type = jsonInfo.mime_type
|
||||
onFinish()
|
||||
},
|
||||
onBeforeRequest: function (req) {
|
||||
const xhr = req.getUnderlyingObject()
|
||||
xhr.withCredentials = withCredentials
|
||||
},
|
||||
})
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0])
|
||||
}
|
||||
upload.start()
|
||||
})
|
||||
}
|
||||
|
||||
function createThumbnailUrl(
|
||||
_file: File | null,
|
||||
fileInfo: UploadSettledFileInfo,
|
||||
): string | undefined {
|
||||
if (!fileInfo) return undefined
|
||||
return fileInfo.url ?? undefined
|
||||
}
|
||||
|
||||
function customDownload(file: UploadFileInfo) {
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function customPreview(file: UploadFileInfo, detail: { event: MouseEvent }) {
|
||||
detail.event.preventDefault()
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function disablePreviousDate(ts: number) {
|
||||
return ts <= Date.now()
|
||||
}
|
||||
</script>
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<n-form :model="formValue" :rules="rules" ref="formRef">
|
||||
<n-form-item label="Slug" path="slug">
|
||||
<n-input v-model:value="formValue.slug" placeholder="Input Slug" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Name" path="name">
|
||||
<n-input v-model:value="formValue.name" placeholder="Input Name" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Description" path="description">
|
||||
<n-input
|
||||
v-model:value="formValue.description"
|
||||
placeholder="Input Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Passcode" path="passcode">
|
||||
<n-input
|
||||
v-model:value="formValue.passcode"
|
||||
placeholder="Input Passcode"
|
||||
type="password"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Expired At" path="expiredAt">
|
||||
<n-date-picker v-model:value="formValue.expiredAt" type="datetime" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NDatePicker,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
} from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const props = defineProps<{ value: any }>()
|
||||
const formValue = ref(props.value)
|
||||
|
||||
const rules: FormRules = {
|
||||
slug: [
|
||||
{
|
||||
max: 1024,
|
||||
message: 'Slug can be at most 1024 characters long',
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{
|
||||
max: 1024,
|
||||
message: 'Name can be at most 1024 characters long',
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{
|
||||
max: 8192,
|
||||
message: 'Description can be at most 8192 characters long',
|
||||
},
|
||||
],
|
||||
passcode: [
|
||||
{
|
||||
max: 256,
|
||||
message: 'Passcode can be at most 256 characters long',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
DyPrefetch?: any
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<n-layout has-sider class="h-full">
|
||||
<n-layout-sider bordered collapse-mode="width" :collapsed-width="64" :width="240" show-trigger>
|
||||
<n-menu
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:value="route.name as string"
|
||||
@update:value="updateMenuSelect"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<router-view />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DataUsageRound,
|
||||
AllInboxFilled,
|
||||
PermDataSettingRound,
|
||||
ShoppingBagRound,
|
||||
} from '@vicons/material'
|
||||
import { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui'
|
||||
import { h, type Component } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function renderIcon(icon: Component) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
label: 'Usage',
|
||||
key: 'dashboardUsage',
|
||||
icon: renderIcon(DataUsageRound),
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
key: 'dashboardFiles',
|
||||
icon: renderIcon(AllInboxFilled),
|
||||
},
|
||||
{
|
||||
label: 'Bundles',
|
||||
key: 'dashboardBundles',
|
||||
icon: renderIcon(ShoppingBagRound),
|
||||
},
|
||||
{
|
||||
label: 'Quota',
|
||||
key: 'dashboardQuota',
|
||||
icon: renderIcon(PermDataSettingRound),
|
||||
},
|
||||
]
|
||||
|
||||
function updateMenuSelect(key: string) {
|
||||
router.push({ name: key })
|
||||
}
|
||||
</script>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<n-layout>
|
||||
<n-layout-header class="border-b-1 flex justify-between items-center">
|
||||
<router-link to="/" class="text-lg font-bold">Solar Network Drive</router-link>
|
||||
<div v-if="!hideUserMenu">
|
||||
<n-dropdown
|
||||
v-if="!userStore.isAuthenticated"
|
||||
:options="guestOptions"
|
||||
@select="handleGuestMenuSelect"
|
||||
>
|
||||
<n-button>Account</n-button>
|
||||
</n-dropdown>
|
||||
<n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary">
|
||||
<n-button>{{ userStore.user.nick }}</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content embedded>
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h } from 'vue'
|
||||
import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui'
|
||||
import {
|
||||
LogInOutlined,
|
||||
PersonAddAlt1Outlined,
|
||||
PersonOutlineRound,
|
||||
DataUsageRound,
|
||||
} from '@vicons/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const hideUserMenu = computed(() => {
|
||||
return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string)
|
||||
})
|
||||
|
||||
const guestOptions = [
|
||||
{
|
||||
label: 'Login',
|
||||
key: 'login',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(LogInOutlined),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Create Account',
|
||||
key: 'create-account',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonAddAlt1Outlined),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
const userOptions = computed(() => [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
key: 'dashboardUsage',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(DataUsageRound),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
key: 'profile',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonOutlineRound),
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
function handleGuestMenuSelect(key: string) {
|
||||
if (key === 'login') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank')
|
||||
} else if (key === 'create-account') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserMenuSelect(key: string) {
|
||||
if (key === 'profile') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank')
|
||||
} else {
|
||||
router.push({ name: key })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.n-layout-header {
|
||||
padding: 8px 24px;
|
||||
border-color: var(--n-border-color);
|
||||
height: 57px; /* Fixed height */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.n-layout-content {
|
||||
height: calc(100vh - 57px); /* Adjust based on header height */
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
import '@fontsource-variable/nunito';
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import Root from './root.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(Root)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutDefault from './layouts/default.vue'
|
||||
|
||||
import { RouterView } from 'vue-router'
|
||||
import {
|
||||
NGlobalStyle,
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
} from 'naive-ui'
|
||||
import { usePreferredDark } from '@vueuse/core'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { onMounted } from 'vue'
|
||||
import { useServicesStore } from './stores/services'
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif',
|
||||
primaryColor: '#7D80BAFF',
|
||||
primaryColorHover: '#9294C5FF',
|
||||
primaryColorPressed: '#575B9DFF',
|
||||
primaryColorSuppl: '#6B6FC1FF',
|
||||
},
|
||||
}
|
||||
|
||||
const isDark = usePreferredDark()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
onMounted(() => {
|
||||
userStore.initialize()
|
||||
|
||||
userStore.fetchUser()
|
||||
servicesStore.fetchServices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
|
||||
<n-global-style />
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider placement="bottom">
|
||||
<layout-default>
|
||||
<router-view />
|
||||
</layout-default>
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import('../views/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/files/:fileId',
|
||||
name: 'files',
|
||||
component: () => import('../views/files.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bundles/:bundleId',
|
||||
name: 'bundleDetails',
|
||||
component: () => import('../views/bundles.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('../layouts/dashboard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'usage',
|
||||
name: 'dashboardUsage',
|
||||
component: () => import('../views/dashboard/usage.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'dashboardFiles',
|
||||
component: () => import('../views/dashboard/files.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'bundles',
|
||||
name: 'dashboardBundles',
|
||||
component: () => import('../views/dashboard/bundles.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'quotas',
|
||||
name: 'dashboardQuota',
|
||||
component: () => import('../views/dashboard/quotas.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:notFound(.*)',
|
||||
name: 'errorNotFound',
|
||||
component: () => import('../views/not-found.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
// Initialize user state if not already initialized
|
||||
if (!userStore.user) {
|
||||
await userStore.fetchUser()
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) {
|
||||
window.open(
|
||||
servicesStore.getSerivceUrl(
|
||||
'DysonNetwork.Pass',
|
||||
'login?redirect=' + encodeURIComponent(window.location.href),
|
||||
)!,
|
||||
'_blank',
|
||||
)
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,27 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useServicesStore = defineStore('services', () => {
|
||||
const services = ref<Record<string, string>>({})
|
||||
|
||||
async function fetchServices() {
|
||||
try {
|
||||
const response = await fetch('/cgi/.well-known/services')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
services.value = data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch services:', error)
|
||||
services.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
function getSerivceUrl(serviceName: string, ...parts: string[]): string | null {
|
||||
const baseUrl = services.value[serviceName] || null
|
||||
return baseUrl ? `${baseUrl}/${parts.join('/')}` : null
|
||||
}
|
||||
|
||||
return { services, fetchServices, getSerivceUrl }
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const user = ref<any>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
// Actions
|
||||
async function fetchUser(reload = true) {
|
||||
if (!reload && user.value) return
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch('/cgi/id/accounts/me', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If the token is invalid, clear it and the user state
|
||||
throw new Error('Failed to fetch user information.')
|
||||
}
|
||||
|
||||
user.value = await response.json()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
user.value = null // Clear user data on error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app'
|
||||
window.addEventListener('message', (event) => {
|
||||
// IMPORTANT: Always check the origin of the message for security!
|
||||
// This prevents malicious scripts from sending fake login status updates.
|
||||
// Ensure event.origin exactly matches your identity service's origin.
|
||||
if (event.origin !== allowedOrigin) {
|
||||
console.warn(`[SYNC] Message received from unexpected origin: ${event.origin}. Ignoring.`)
|
||||
return // Ignore messages from unknown origins
|
||||
}
|
||||
|
||||
// Check if the message is the type we're expecting
|
||||
if (event.data && event.data.type === 'DY:LOGIN_STATUS_CHANGE') {
|
||||
const { loggedIn } = event.data
|
||||
console.log(`[SYNC] Received login status change: ${loggedIn}`)
|
||||
fetchUser() // Re-fetch user data on login status change
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fetchUser,
|
||||
initialize,
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
export interface SnFilePool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
storage_config: StorageConfig
|
||||
billing_config: BillingConfig
|
||||
policy_config: any
|
||||
public_indexable: boolean
|
||||
public_usable: boolean
|
||||
no_optimization: boolean
|
||||
no_metadata: boolean
|
||||
allow_encryption: boolean
|
||||
allow_anonymous: boolean
|
||||
require_privilege: number
|
||||
account_id: null
|
||||
resource_identifier: string
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at: null
|
||||
}
|
||||
|
||||
export interface BillingConfig {
|
||||
cost_multiplier: number
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
region: string
|
||||
bucket: string
|
||||
endpoint: string
|
||||
secret_id: string
|
||||
secret_key: string
|
||||
enable_signed: boolean
|
||||
enable_ssl: boolean
|
||||
image_proxy: null
|
||||
access_proxy: null
|
||||
expiration: null
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
<template>
|
||||
<section class="min-h-full relative flex items-center justify-center">
|
||||
<n-spin v-if="!bundleInfo && !error" />
|
||||
<n-result
|
||||
status="404"
|
||||
title="No bundle was found"
|
||||
:description="error"
|
||||
v-else-if="error === '404'"
|
||||
/>
|
||||
|
||||
<n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'">
|
||||
<n-result
|
||||
status="403"
|
||||
title="Access Denied"
|
||||
description="This bundle is protected by a passcode"
|
||||
class="mt-5 mb-2"
|
||||
>
|
||||
<template #footer>
|
||||
<n-alert v-if="passcodeError" type="error" class="mb-3">
|
||||
{{ passcodeError }}
|
||||
</n-alert>
|
||||
<n-input
|
||||
v-model:value="passcode"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="Passcode"
|
||||
@keyup.enter="fetchBundleInfo"
|
||||
class="mb-3"
|
||||
/>
|
||||
<n-button type="primary" block @click="fetchBundleInfo">Access Bundle</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</n-card>
|
||||
|
||||
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
||||
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
||||
<n-gi>
|
||||
<n-card title="Content" size="small">
|
||||
<n-list
|
||||
size="small"
|
||||
v-if="bundleInfo.files && bundleInfo.files.length > 0"
|
||||
style="padding: 0"
|
||||
>
|
||||
<n-list-item v-for="file in bundleInfo.files" :key="file.id">
|
||||
<n-thing :title="file.name" :description="formatBytes(file.size)">
|
||||
<template #header-extra>
|
||||
<n-button text type="primary" @click="goToFileDetails(file.id)">View</n-button>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-empty v-else description="No files in this bundle" />
|
||||
<template #footer>
|
||||
<n-collapse-transition :show="!!downloadProgress">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="downloadProgress"
|
||||
indicator-placement="inside"
|
||||
:status="downloadStatus"
|
||||
processing
|
||||
class="mb-4"
|
||||
/>
|
||||
</n-collapse-transition>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:disabled="!bundleInfo.files || bundleInfo.files.length === 0 || downloading"
|
||||
@click="downloadAllFiles"
|
||||
>
|
||||
Download All
|
||||
</n-button>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<n-card size="small">
|
||||
<h3 class="text-lg">{{ bundleInfo.name }}</h3>
|
||||
<p class="mb-3" v-if="bundleInfo.description">{{ bundleInfo.description }}</p>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<calendar-today-round />
|
||||
</n-icon>
|
||||
Expires At
|
||||
</span>
|
||||
<span>{{
|
||||
bundleInfo.expiredAt ? new Date(bundleInfo.expiredAt).toLocaleString() : 'Never'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<lock-round />
|
||||
</n-icon>
|
||||
Passcode Protected
|
||||
</span>
|
||||
<span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-input
|
||||
v-model:value="filePass"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="File password file decrypt"
|
||||
class="mt-3"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCard,
|
||||
NResult,
|
||||
NSpin,
|
||||
NIcon,
|
||||
NGrid,
|
||||
NGi,
|
||||
NList,
|
||||
NListItem,
|
||||
NThing,
|
||||
NButton,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NAlert,
|
||||
NProgress,
|
||||
NCollapseTransition,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { CalendarTodayRound, LockRound } from '@vicons/material'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { formatBytes } from './format' // Assuming format.ts is in the same directory
|
||||
import { downloadAndDecryptFile } from './secure'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
const bundleId = route.params.bundleId
|
||||
const passcode = ref<string>('')
|
||||
const passcodeError = ref<string | null>(null)
|
||||
|
||||
const filePass = ref<string>('')
|
||||
|
||||
const downloading = ref(false)
|
||||
const downloadProgress = ref<number | undefined>()
|
||||
const downloadStatus = ref<'success' | 'error' | 'info'>('info')
|
||||
|
||||
watch(
|
||||
route,
|
||||
(value) => {
|
||||
if (value.query.passcode) passcode.value = value.query.passcode.toString()
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
const bundleInfo = ref<any>(null)
|
||||
async function fetchBundleInfo() {
|
||||
try {
|
||||
let url = '/api/bundles/' + bundleId
|
||||
if (passcode.value) {
|
||||
url += `?passcode=${passcode.value}`
|
||||
}
|
||||
const resp = await fetch(url)
|
||||
if (resp.status === 403) {
|
||||
error.value = '403'
|
||||
bundleInfo.value = null
|
||||
if (passcode.value) {
|
||||
passcodeError.value = 'Incorrect passcode.'
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to fetch bundle info: ' + resp.statusText)
|
||||
}
|
||||
bundleInfo.value = await resp.json()
|
||||
error.value = null
|
||||
passcodeError.value = null
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchBundleInfo())
|
||||
|
||||
function goToFileDetails(fileId: string) {
|
||||
router.push({ path: `/files/${fileId}`, query: { passcode: passcode.value } })
|
||||
}
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
async function downloadAllFiles() {
|
||||
if (!bundleInfo.value || !bundleInfo.value.files || bundleInfo.value.files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
downloading.value = true
|
||||
downloadProgress.value = 0
|
||||
downloadStatus.value = 'info'
|
||||
|
||||
const totalFiles = bundleInfo.value.files.length
|
||||
let completedDownloads = 0
|
||||
|
||||
for (const file of bundleInfo.value.files) {
|
||||
let url = `/api/files/${file.id}`
|
||||
if (passcode.value) {
|
||||
url += `?passcode=${passcode.value}`
|
||||
}
|
||||
|
||||
if (file.is_encrypted) {
|
||||
downloadAndDecryptFile(file, filePass.value, file.name, () => {})
|
||||
.catch((err) => {
|
||||
messageDisplay.error('Download failed: ' + err.message, {
|
||||
closable: true,
|
||||
duration: 10000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
completedDownloads++
|
||||
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download ${file.name}: ${res.statusText}`)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = file.name || 'download' // fallback name
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
|
||||
if (completedDownloads === totalFiles) {
|
||||
downloadStatus.value = 'success'
|
||||
}
|
||||
} catch (err) {
|
||||
messageDisplay.error(`Download failed for ${file.name}: ${err}`)
|
||||
downloadStatus.value = 'error'
|
||||
} finally {
|
||||
completedDownloads++
|
||||
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,180 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="bundles"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NDataTable,
|
||||
type DataTableColumns,
|
||||
type PaginationProps,
|
||||
useMessage,
|
||||
useLoadingBar,
|
||||
NButton,
|
||||
NIcon,
|
||||
NSpace,
|
||||
useDialog,
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DeleteRound } from '@vicons/material'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const bundles = ref<any[]>([])
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render(row: any) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
router.push(`/bundles/${row.id}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
},
|
||||
)
|
||||
},
|
||||
maxWidth: 80,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
key: 'description',
|
||||
maxWidth: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Updated At',
|
||||
key: 'updated_at',
|
||||
render(row: any) {
|
||||
return new Date(row.updated_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
render(row: any) {
|
||||
return h(NSpace, {}, [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
askDeleteBundle(row)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
|
||||
},
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchBundles() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/bundles/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
bundles.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
messageDialog.error('Failed to fetch bundles: ' + (error as Error).message)
|
||||
console.error('Failed to fetch bundles:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchBundles())
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchBundles()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const messageDialog = useMessage()
|
||||
const loadingBar = useLoadingBar()
|
||||
const dialog = useDialog()
|
||||
|
||||
function askDeleteBundle(bundle: any) {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want to delete the bundle ${bundle.name}?`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
onPositiveClick: () => {
|
||||
deleteBundle(bundle)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteBundle(bundle: any) {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch(`/api/bundles/${bundle.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
tablePagination.value.page = 1
|
||||
await fetchBundles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success('Bundle deleted successfully')
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete bundle: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,304 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<file-pool-select
|
||||
v-model="filePool"
|
||||
placeholder="Filter by file pool"
|
||||
size="medium"
|
||||
class="max-w-[480px]"
|
||||
@update:pool="fetchFiles"
|
||||
/>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<n-switch size="large" v-model:value="showRecycled">
|
||||
<template #checked>Recycled</template>
|
||||
<template #unchecked>Unrecycled</template>
|
||||
</n-switch>
|
||||
<n-button
|
||||
@click="askDeleteRecycledFiles"
|
||||
v-if="showRecycled"
|
||||
type="error"
|
||||
circle
|
||||
size="small"
|
||||
>
|
||||
<n-icon>
|
||||
<delete-sweep-round />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="files"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NDataTable,
|
||||
NIcon,
|
||||
NImage,
|
||||
NButton,
|
||||
NSpace,
|
||||
type DataTableColumns,
|
||||
type PaginationProps,
|
||||
useDialog,
|
||||
useMessage,
|
||||
useLoadingBar,
|
||||
NSwitch,
|
||||
NTooltip,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
AudioFileRound,
|
||||
InsertDriveFileRound,
|
||||
VideoFileRound,
|
||||
FileDownloadOutlined,
|
||||
DeleteRound,
|
||||
DeleteSweepRound,
|
||||
} from '@vicons/material'
|
||||
import { h, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatBytes } from '../format'
|
||||
import FilePoolSelect from '@/components/FilePoolSelect.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const files = ref<any[]>([])
|
||||
|
||||
const filePool = ref<string | null>(null)
|
||||
const showRecycled = ref(false)
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Preview',
|
||||
key: 'preview',
|
||||
render(row: any) {
|
||||
switch (row.mime_type.split('/')[0]) {
|
||||
case 'image':
|
||||
return h(NImage, {
|
||||
src: '/api/files/' + row.id,
|
||||
width: 32,
|
||||
height: 32,
|
||||
objectFit: 'contain',
|
||||
style: { aspectRatio: 1 },
|
||||
})
|
||||
case 'video':
|
||||
return h(NIcon, { size: 32 }, { default: () => h(VideoFileRound) })
|
||||
case 'audio':
|
||||
return h(NIcon, { size: 32 }, { default: () => h(AudioFileRound) })
|
||||
default:
|
||||
return h(NIcon, { size: 32 }, { default: () => h(InsertDriveFileRound) })
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
maxWidth: 180,
|
||||
ellipsis: true,
|
||||
render(row: any) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
router.push(`/files/${row.id}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Size',
|
||||
key: 'size',
|
||||
render(row: any) {
|
||||
return formatBytes(row.size)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pool',
|
||||
key: 'pool',
|
||||
render(row: any) {
|
||||
if (!row.pool) return 'Unstored'
|
||||
return h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
default: () => h('span', row.pool.id),
|
||||
trigger: () => h('span', row.pool.name),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Uploaded At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
render(row: any) {
|
||||
return h(NSpace, {}, [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
onClick: () => {
|
||||
window.open(`/api/files/${row.id}`, '_blank')
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(FileDownloadOutlined) }),
|
||||
},
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
askDeleteFile(row)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
|
||||
},
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchFiles() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/files/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}&recycled=${showRecycled.value}${filePool.value ? '&pool=' + filePool.value : ''}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
files.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch files:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchFiles())
|
||||
|
||||
watch(showRecycled, () => {
|
||||
tablePagination.value.itemCount = 0
|
||||
tablePagination.value.page = 1
|
||||
fetchFiles()
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchFiles()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const dialog = useDialog()
|
||||
const messageDialog = useMessage()
|
||||
const loadingBar = useLoadingBar()
|
||||
|
||||
function askDeleteFile(file: any) {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want delete ${file.name}? This will delete the stored file data immediately, there is no return.`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
draggable: true,
|
||||
onPositiveClick: () => {
|
||||
deleteFile(file)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteFile(file: any) {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch(`/api/files/${file.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
tablePagination.value.page = 1
|
||||
await fetchFiles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success('File deleted successfully')
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete file: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
function askDeleteRecycledFiles() {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want to delete all ${tablePagination.value.itemCount} marked recycled file(s) by system?`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
draggable: true,
|
||||
onPositiveClick: () => {
|
||||
deleteRecycledFiles()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteRecycledFiles() {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch('/api/files/me/recycle', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const resp = await response.json()
|
||||
tablePagination.value.page = 1
|
||||
await fetchFiles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success(`Recycled files deleted successfully, deleted count: ${resp.count}`)
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete recycled files: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="quotas"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NDataTable, type DataTableColumns, type PaginationProps, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { formatBytes } from '../format'
|
||||
|
||||
const quotas = ref<any[]>([])
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Quota',
|
||||
key: 'quota',
|
||||
render(row: any) {
|
||||
return formatBytes(row.quota * 1024 * 1024)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Updated At',
|
||||
key: 'updated_at',
|
||||
render(row: any) {
|
||||
return new Date(row.updated_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchQuotas() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/billing/quota/records?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
quotas.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
messageDialog.error('Failed to fetch quotas: ' + (error as Error).message)
|
||||
console.error('Failed to fetch quotas:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchQuotas())
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchQuotas()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const messageDialog = useMessage()
|
||||
</script>
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full container-fluid mx-auto py-4 px-5">
|
||||
<div class="h-full flex justify-center items-center" v-if="!usage">
|
||||
<n-spin />
|
||||
</div>
|
||||
<n-grid cols="1 s:2 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else>
|
||||
<n-gi span="4">
|
||||
<n-alert title="Billing Tips" size="small" type="info" closable>
|
||||
<p>
|
||||
The minimal billable unit is MiB, if your file is not enough 1 MiB it will be counted as
|
||||
1 MiB.
|
||||
</p>
|
||||
<p>The <b>1 MiB = 1024 KiB = 1,048,576 B</b></p>
|
||||
</n-alert>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="All Uploads" tabular-nums>
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="toGigabytes(usage.total_usage_bytes)"
|
||||
:precision="3"
|
||||
/>
|
||||
<template #suffix>GiB</template>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="All Files" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_file_count" />
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_quota" />
|
||||
<template #suffix>MiB</template>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<div class="flex gap-2 justify-between items-end">
|
||||
<n-statistic label="Used Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="quotaUsagePercentage" :precision="2" />
|
||||
<template #suffix>%</template>
|
||||
</n-statistic>
|
||||
<n-progress
|
||||
type="circle"
|
||||
:percentage="quotaUsagePercentage"
|
||||
:show-indicator="false"
|
||||
:stroke-width="16"
|
||||
style="width: 40px"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="aspect-video" title="Pool Usage">
|
||||
<pie
|
||||
:data="poolChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
|
||||
}"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="aspect-video h-full" title="Verbose Quota">
|
||||
<pie
|
||||
:data="quotaChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
|
||||
}"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation, NAlert, NProgress } from 'naive-ui'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement } from 'chart.js'
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, ArcElement)
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isDesktop = breakpoints.greaterOrEqual('md')
|
||||
|
||||
const poolChartData = computed(() => ({
|
||||
labels: usage.value.pool_usages.map((pool: any) => pool.pool_name),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Pool Usage',
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: usage.value.pool_usages.map((pool: any) => pool.usage_bytes),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const usage = ref<any>()
|
||||
async function fetchUsage() {
|
||||
try {
|
||||
const response = await fetch('/api/billing/usage')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
usage.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch usage data:', error)
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchUsage())
|
||||
|
||||
const verboseQuota = ref<
|
||||
{ based_quota: number; extra_quota: number; total_quota: number } | undefined
|
||||
>()
|
||||
async function fetchVerboseQuota() {
|
||||
try {
|
||||
const response = await fetch('/api/billing/quota')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
verboseQuota.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch verbose data:', error)
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchVerboseQuota())
|
||||
|
||||
const quotaChartData = computed(() => ({
|
||||
labels: ['Base Quota', 'Extra Quota'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Verbose Quota',
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: [verboseQuota.value?.based_quota ?? 0, verboseQuota.value?.extra_quota ?? 0],
|
||||
},
|
||||
],
|
||||
}))
|
||||
const quotaUsagePercentage = computed(
|
||||
() => (usage.value.used_quota / usage.value.total_quota) * 100,
|
||||
)
|
||||
|
||||
function toGigabytes(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-stats {
|
||||
height: 105px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<section class="min-h-full relative flex items-center justify-center">
|
||||
<n-spin v-if="!fileInfo && !error" />
|
||||
<n-result status="404" title="No file was found" :description="error" v-else-if="error" />
|
||||
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
||||
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
||||
<n-gi>
|
||||
<div v-if="fileInfo.is_encrypted">
|
||||
<n-alert type="info" size="small" title="Encrypted file">
|
||||
The file has been encrypted. Preview not available. Please enter the password to
|
||||
download it.
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" />
|
||||
<video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" />
|
||||
<audio v-else-if="fileType === 'audio'" :src="fileSource" controls class="w-full" />
|
||||
<n-result
|
||||
status="418"
|
||||
title="Preview Unavailable"
|
||||
description="How can you preview this file?"
|
||||
size="small"
|
||||
class="py-6"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<div class="mb-3">
|
||||
<n-card title="File Infomation" size="small">
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<info-round />
|
||||
</n-icon>
|
||||
File Type
|
||||
</span>
|
||||
<span>{{ fileInfo.mime_type }} ({{ fileType }})</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<data-usage-round />
|
||||
</n-icon>
|
||||
File Size
|
||||
</span>
|
||||
<span>{{ formatBytes(fileInfo.size) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<file-upload-outlined />
|
||||
</n-icon>
|
||||
Uploaded At
|
||||
</span>
|
||||
<span>{{ new Date(fileInfo.created_at).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<details-round />
|
||||
</n-icon>
|
||||
Techical Info
|
||||
</span>
|
||||
<n-button text size="small" @click="showTechDetails = !showTechDetails">
|
||||
{{ showTechDetails ? 'Hide' : 'Show' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-collapse-transition :show="showTechDetails">
|
||||
<div v-if="showTechDetails" class="mt-2 flex flex-col gap-1">
|
||||
<p class="text-xs opacity-75">#{{ fileInfo.id }}</p>
|
||||
|
||||
<n-card size="small" content-style="padding: 0" embedded>
|
||||
<div class="overflow-x-auto px-4 py-2">
|
||||
<n-code
|
||||
:code="JSON.stringify(fileInfo.file_meta, null, 4)"
|
||||
language="json"
|
||||
:hljs="hljs"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<n-input
|
||||
v-if="fileInfo.is_encrypted"
|
||||
placeholder="Password"
|
||||
v-model:value="filePass"
|
||||
type="password"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<n-button class="flex-grow-1" @click="downloadFile">Download</n-button>
|
||||
<n-popover placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button>
|
||||
<n-icon>
|
||||
<qr-code-round />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-qr-code
|
||||
type="svg"
|
||||
:value="currentUrl"
|
||||
:size="160"
|
||||
icon-src="/favicon.png"
|
||||
error-correction-level="H"
|
||||
/>
|
||||
</n-popover>
|
||||
</div>
|
||||
</div>
|
||||
<n-collapse-transition :show="!!progress">
|
||||
<n-progress
|
||||
:processing="!!progress && progress < 100"
|
||||
:percentage="progress"
|
||||
indicator-placement="inside"
|
||||
class="mt-4"
|
||||
/>
|
||||
</n-collapse-transition>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCard,
|
||||
NInput,
|
||||
NButton,
|
||||
NProgress,
|
||||
NResult,
|
||||
NSpin,
|
||||
NImage,
|
||||
NAlert,
|
||||
NIcon,
|
||||
NCollapseTransition,
|
||||
NCode,
|
||||
NGrid,
|
||||
NGi,
|
||||
NPopover,
|
||||
NQrCode,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
DataUsageRound,
|
||||
InfoRound,
|
||||
DetailsRound,
|
||||
FileUploadOutlined,
|
||||
QrCodeRound,
|
||||
} from '@vicons/material'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { downloadAndDecryptFile } from './secure'
|
||||
import { formatBytes } from './format'
|
||||
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
|
||||
hljs.registerLanguage('json', json)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const filePass = ref<string>('')
|
||||
const fileId = route.params.fileId
|
||||
const passcode = route.query.passcode as string | undefined
|
||||
|
||||
const progress = ref<number | undefined>(0)
|
||||
|
||||
const showTechDetails = ref<boolean>(false)
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
const currentUrl = window.location.href
|
||||
|
||||
const fileInfo = ref<any>(null)
|
||||
async function fetchFileInfo() {
|
||||
try {
|
||||
let url = '/api/files/' + fileId + '/info'
|
||||
if (passcode) {
|
||||
url += `?passcode=${passcode}`
|
||||
}
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to fetch file info: ' + resp.statusText)
|
||||
}
|
||||
fileInfo.value = await resp.json()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchFileInfo())
|
||||
|
||||
const fileType = computed(() => {
|
||||
if (!fileInfo.value) return 'unknown'
|
||||
return fileInfo.value.mime_type?.split('/')[0] || 'unknown'
|
||||
})
|
||||
const fileSource = computed(() => {
|
||||
let url = `/api/files/${fileId}`
|
||||
if (passcode) {
|
||||
url += `?passcode=${passcode}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
async function downloadFile() {
|
||||
if (fileInfo.value.is_encrypted && !filePass.value) {
|
||||
messageDisplay.error('Please enter the password to download the file.')
|
||||
return
|
||||
}
|
||||
if (fileInfo.value.is_encrypted) {
|
||||
downloadAndDecryptFile(fileSource.value, filePass.value, fileInfo.value.name, (p: number) => {
|
||||
progress.value = p * 100
|
||||
}).catch((err) => {
|
||||
messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 })
|
||||
progress.value = undefined
|
||||
})
|
||||
} else {
|
||||
const res = await fetch(fileSource.value)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download ${fileInfo.value.name}: ${res.statusText}`)
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length')
|
||||
if (!contentLength) {
|
||||
throw new Error('Content-Length response header is missing.')
|
||||
}
|
||||
|
||||
const total = parseInt(contentLength, 10)
|
||||
const reader = res.body!.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
let received = 0
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (value) {
|
||||
chunks.push(value)
|
||||
received += value.length
|
||||
progress.value = (received / total) * 100
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob(chunks)
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = fileInfo.value.name || 'download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full relative flex flex-col items-center justify-center">
|
||||
<n-card class="max-w-lg my-4 mx-8" title="About" v-if="!userStore.user">
|
||||
<p>Welcome to the <b>Solar Drive</b></p>
|
||||
<p>We help you upload, collect, and share files with ease in mind.</p>
|
||||
<p>To continue, login first.</p>
|
||||
</n-card>
|
||||
|
||||
<n-card class="max-w-2xl" v-else content-style="padding: 0;">
|
||||
<n-tabs type="line" animated :tabs-padding="20" pane-style="padding: 20px">
|
||||
<template #suffix>
|
||||
<div class="flex gap-2 items-center me-4">
|
||||
<p>Advance Mode</p>
|
||||
<n-switch v-model:value="modeAdvanced" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-tab-pane name="direct" tab="Direct Upload" :disabled="isBundleMode">
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bundle" tab="Bundle Upload">
|
||||
<div class="mb-3">
|
||||
<bundle-select v-model:bundle="selectedBundleId" :disabled="isBundleMode" />
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="showCreateBundleModal" preset="dialog" title="Create New Bundle">
|
||||
<bundle-form ref="bundleFormRef" :value="newBundle" />
|
||||
<template #action>
|
||||
<n-button @click="showCreateBundleModal = false">Cancel</n-button>
|
||||
<n-button type="primary" @click="createBundle">Create</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<n-button @click="showCreateBundleModal = true" class="mb-3" :disabled="isBundleMode">
|
||||
Create New Bundle
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedBundleId && !newBundleId && !isBundleMode"
|
||||
@click="isBundleMode ? cancelBundleUpload() : proceedToBundleUpload()"
|
||||
>
|
||||
{{ isBundleMode ? 'Cancel' : 'Proceed to Upload' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="bundleUploadMode" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
:bundleId="currentBundleId!"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NCard, NSwitch, NTabs, NTabPane, NButton, NModal } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
import FilePoolSelect from '@/components/FilePoolSelect.vue'
|
||||
import UploadArea from '@/components/UploadArea.vue'
|
||||
import BundleSelect from '@/components/BundleSelect.vue'
|
||||
import BundleForm from '@/components/form/BundleForm.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const version = ref<any>(null)
|
||||
async function fetchVersion() {
|
||||
const resp = await fetch('/api/version')
|
||||
version.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchVersion())
|
||||
|
||||
type SnFilePoolOption = SnFilePool & any
|
||||
|
||||
const pools = ref<SnFilePoolOption[] | undefined>()
|
||||
async function fetchPools() {
|
||||
const resp = await fetch('/api/pools')
|
||||
pools.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchPools())
|
||||
|
||||
const modeAdvanced = ref(false)
|
||||
|
||||
const filePool = ref<string | null>(null)
|
||||
|
||||
const currentFilePool = computed(() => {
|
||||
if (!filePool.value) return null
|
||||
return pools.value?.find((pool) => pool.id === filePool.value) ?? null
|
||||
})
|
||||
|
||||
const bundles = ref<any>([])
|
||||
const selectedBundleId = ref<string | null>(null)
|
||||
const showCreateBundleModal = ref(false)
|
||||
const newBundle = ref<any>({})
|
||||
const bundleFormRef = ref<any>(null)
|
||||
const bundleUploadMode = ref(false)
|
||||
const currentBundleId = ref<string | null>(null)
|
||||
const newBundleId = ref<string | null>(null)
|
||||
const isBundleMode = ref(false)
|
||||
|
||||
async function createBundle() {
|
||||
try {
|
||||
await bundleFormRef.value?.formRef?.validate()
|
||||
const resp = await fetch('/api/bundles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newBundle.value),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to create bundle')
|
||||
}
|
||||
const createdBundle = await resp.json()
|
||||
bundles.value.push(createdBundle)
|
||||
selectedBundleId.value = createdBundle.id
|
||||
newBundleId.value = createdBundle.id
|
||||
showCreateBundleModal.value = false
|
||||
newBundle.value = {}
|
||||
} catch (error) {
|
||||
console.error('Failed to create bundle:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function proceedToBundleUpload() {
|
||||
currentBundleId.value = selectedBundleId.value || newBundleId.value
|
||||
bundleUploadMode.value = true
|
||||
isBundleMode.value = true
|
||||
}
|
||||
|
||||
function cancelBundleUpload() {
|
||||
bundleUploadMode.value = false
|
||||
isBundleMode.value = false
|
||||
currentBundleId.value = null
|
||||
selectedBundleId.value = null
|
||||
newBundleId.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full flex items-center justify-center">
|
||||
<n-result status="404" title="404" description="Page not found">
|
||||
<template #footer>
|
||||
<n-button @click="router.push('/')">Go to Home</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NResult, NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
@@ -1,94 +0,0 @@
|
||||
export async function downloadAndDecryptFile(
|
||||
url: string,
|
||||
password: string,
|
||||
fileName: string,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<void> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
|
||||
|
||||
const contentLength = +(response.headers.get('Content-Length') || 0)
|
||||
const reader = response.body!.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
let received = 0
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (value) {
|
||||
chunks.push(value)
|
||||
received += value.length
|
||||
if (contentLength && onProgress) {
|
||||
onProgress(received / contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fullBuffer = new Uint8Array(received)
|
||||
let offset = 0
|
||||
for (const chunk of chunks) {
|
||||
fullBuffer.set(chunk, offset)
|
||||
offset += chunk.length
|
||||
}
|
||||
|
||||
const decryptedBytes = await decryptFile(fullBuffer, password)
|
||||
|
||||
// Create a blob and trigger a download
|
||||
const blob = new Blob([decryptedBytes])
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
}
|
||||
|
||||
export async function decryptFile(fileBuffer: Uint8Array, password: string): Promise<Uint8Array> {
|
||||
const salt = fileBuffer.slice(0, 16)
|
||||
const nonce = fileBuffer.slice(16, 28)
|
||||
const tag = fileBuffer.slice(28, 44)
|
||||
const ciphertext = fileBuffer.slice(44)
|
||||
|
||||
const enc = new TextEncoder()
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
enc.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey'],
|
||||
)
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['decrypt'],
|
||||
)
|
||||
|
||||
const fullCiphertext = new Uint8Array(ciphertext.length + tag.length)
|
||||
fullCiphertext.set(ciphertext)
|
||||
fullCiphertext.set(tag, ciphertext.length)
|
||||
|
||||
let decrypted: ArrayBuffer
|
||||
try {
|
||||
decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: nonce, tagLength: 128 },
|
||||
key,
|
||||
fullCiphertext,
|
||||
)
|
||||
} catch {
|
||||
throw new Error('Incorrect password or corrupted file.')
|
||||
}
|
||||
|
||||
const magic = new TextEncoder().encode('DYSON1')
|
||||
const decryptedBytes = new Uint8Array(decrypted)
|
||||
for (let i = 0; i < magic.length; i++) {
|
||||
if (decryptedBytes[i] !== magic[i]) {
|
||||
throw new Error('Incorrect password or corrupted file.')
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedBytes.slice(magic.length)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./**/*.d.ts"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/',
|
||||
plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5090',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cgi': {
|
||||
target: 'http://localhost:5090',
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
# Stage 1: Install runtime dependencies
|
||||
|
||||
# Install only necessary dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libfontconfig1 \
|
||||
@@ -17,34 +19,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
USER $APP_UID
|
||||
|
||||
# Stage 2: Build SPA
|
||||
FROM node:22-alpine AS spa-builder
|
||||
WORKDIR /src
|
||||
|
||||
# Copy package files for SPA
|
||||
COPY ["DysonNetwork.Drive/Client/package.json", "DysonNetwork.Drive/Client/package-lock.json*", "./Client/"]
|
||||
|
||||
# Install SPA dependencies
|
||||
WORKDIR /src/Client
|
||||
RUN npm install
|
||||
|
||||
# Copy SPA source
|
||||
COPY ["DysonNetwork.Drive/Client/", "./"]
|
||||
|
||||
# Build SPA
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Build .NET application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
# Stage 2: Build .NET application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"]
|
||||
RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj"
|
||||
COPY . .
|
||||
|
||||
# Copy built SPA to wwwroot
|
||||
COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Drive/wwwroot/dist
|
||||
|
||||
WORKDIR "/src/DysonNetwork.Drive"
|
||||
RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \
|
||||
-p:TypeScriptCompileBlocked=true \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
@@ -10,53 +10,35 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeKit" Version="4.14.0" />
|
||||
<PackageReference Include="MimeTypes" Version="2.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="6.0.5" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.3" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
|
||||
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<!-- Pin the SkiaSharp version at the 2.88.9 due to the BlurHash need this specific version -->
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -66,16 +48,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\AccountDeletionEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\ContactVerificationEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\EmailLayout.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\LandingEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\PasswordResetEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\VerificationEmail.razor" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
585
DysonNetwork.Drive/Index/FileIndexController.cs
Normal file
585
DysonNetwork.Drive/Index/FileIndexController.cs
Normal file
@@ -0,0 +1,585 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Drive.Index;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/index")]
|
||||
[Authorize]
|
||||
public class FileIndexController(
|
||||
FileIndexService fileIndexService,
|
||||
AppDatabase db,
|
||||
ILogger<FileIndexController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets files in a specific path for the current user
|
||||
/// </summary>
|
||||
/// <param name="path">The path to browse (defaults to root "/")</param>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of files in the specified path</returns>
|
||||
[HttpGet("browse")]
|
||||
public async Task<IActionResult> BrowseFiles(
|
||||
[FromQuery] string path = "/",
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
fileIndexes = fileIndexes
|
||||
.Where(fi => fi.File.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
fileIndexes = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
|
||||
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
|
||||
};
|
||||
|
||||
// Get all file indexes for this account to extract child folders
|
||||
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||
|
||||
// Extract unique child folder paths
|
||||
var childFolders = ExtractChildFolders(allFileIndexes, path);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Path = path,
|
||||
Files = fileIndexes,
|
||||
Folders = childFolders,
|
||||
TotalCount = fileIndexes.Count
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to browse files for account {AccountId} at path {Path}", accountId, path);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "BROWSE_FAILED",
|
||||
Message = "Failed to browse files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts unique child folder paths from all file indexes for a given parent path
|
||||
/// </summary>
|
||||
/// <param name="allFileIndexes">All file indexes for the account</param>
|
||||
/// <param name="parentPath">The parent path to find children for</param>
|
||||
/// <returns>List of unique child folder names</returns>
|
||||
private List<string> ExtractChildFolders(List<SnCloudFileIndex> allFileIndexes, string parentPath)
|
||||
{
|
||||
var normalizedParentPath = FileIndexService.NormalizePath(parentPath);
|
||||
var childFolders = new HashSet<string>();
|
||||
|
||||
foreach (var index in allFileIndexes)
|
||||
{
|
||||
var normalizedIndexPath = FileIndexService.NormalizePath(index.Path);
|
||||
|
||||
// Check if this path is a direct child of the parent path
|
||||
if (normalizedIndexPath.StartsWith(normalizedParentPath) &&
|
||||
normalizedIndexPath != normalizedParentPath)
|
||||
{
|
||||
// Remove the parent path prefix to get the relative path
|
||||
var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length);
|
||||
|
||||
// Extract the first folder name (direct child)
|
||||
var firstSlashIndex = relativePath.IndexOf('/');
|
||||
if (firstSlashIndex > 0)
|
||||
{
|
||||
var folderName = relativePath.Substring(0, firstSlashIndex);
|
||||
childFolders.Add(folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return childFolders.OrderBy(f => f).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files for the current user (across all paths)
|
||||
/// </summary>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of all files for the user</returns>
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> GetAllFiles(
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var fileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
fileIndexes = fileIndexes
|
||||
.Where(fi => fi.File.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
fileIndexes = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
|
||||
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
|
||||
};
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Files = fileIndexes,
|
||||
TotalCount = fileIndexes.Count()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get all files for account {AccountId}", accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "GET_ALL_FAILED",
|
||||
Message = "Failed to get files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets files that have not been indexed for the current user.
|
||||
/// </summary>
|
||||
/// <param name="recycled">Shows recycled files or not</param>
|
||||
/// <param name="offset">The number of files to skip</param>
|
||||
/// <param name="take">The number of files to return</param>
|
||||
/// <param name="pool">The pool ID of those files</param>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of unindexed files</returns>
|
||||
[HttpGet("unindexed")]
|
||||
public async Task<IActionResult> GetUnindexedFiles(
|
||||
[FromQuery] Guid? pool,
|
||||
[FromQuery] bool recycled = false,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var filesQuery = db.Files
|
||||
.Where(f => f.AccountId == accountId
|
||||
&& f.IsMarkedRecycle == recycled
|
||||
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)
|
||||
)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply sorting
|
||||
filesQuery = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? filesQuery.OrderByDescending(f => f.Name)
|
||||
: filesQuery.OrderBy(f => f.Name),
|
||||
"size" => orderDesc ? filesQuery.OrderByDescending(f => f.Size)
|
||||
: filesQuery.OrderBy(f => f.Size),
|
||||
_ => orderDesc ? filesQuery.OrderByDescending(f => f.CreatedAt)
|
||||
: filesQuery.OrderBy(f => f.CreatedAt)
|
||||
};
|
||||
|
||||
if (pool.HasValue) filesQuery = filesQuery.Where(f => f.PoolId == pool);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
filesQuery = filesQuery.Where(f => f.Name.Contains(query));
|
||||
}
|
||||
|
||||
var totalCount = await filesQuery.CountAsync();
|
||||
|
||||
Response.Headers.Append("X-Total", totalCount.ToString());
|
||||
|
||||
var unindexedFiles = await filesQuery
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(unindexedFiles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get unindexed files for account {AccountId}", accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "GET_UNINDEXED_FAILED",
|
||||
Message = "Failed to get unindexed files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a file to a new path
|
||||
/// </summary>
|
||||
/// <param name="indexId">The file index ID</param>
|
||||
/// <param name="newPath">The new path</param>
|
||||
/// <returns>The updated file index</returns>
|
||||
[HttpPost("move/{indexId}")]
|
||||
public async Task<IActionResult> MoveFile(Guid indexId, [FromBody] MoveFileRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify ownership
|
||||
var existingIndex = await db.FileIndexes
|
||||
.Include(fi => fi.File)
|
||||
.FirstOrDefaultAsync(fi => fi.Id == indexId && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
var updatedIndex = await fileIndexService.UpdateAsync(indexId, request.NewPath);
|
||||
|
||||
if (updatedIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
updatedIndex.FileId,
|
||||
IndexId = updatedIndex.Id,
|
||||
OldPath = existingIndex.Path,
|
||||
NewPath = updatedIndex.Path,
|
||||
Message = "File moved successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to move file index {IndexId} for account {AccountId}", indexId, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "MOVE_FAILED",
|
||||
Message = "Failed to move file",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a file index (does not delete the actual file by default)
|
||||
/// </summary>
|
||||
/// <param name="indexId">The file index ID</param>
|
||||
/// <param name="deleteFile">Whether to also delete the actual file data</param>
|
||||
/// <returns>Success message</returns>
|
||||
[HttpDelete("remove/{indexId}")]
|
||||
public async Task<IActionResult> RemoveFileIndex(Guid indexId, [FromQuery] bool deleteFile = false)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify ownership
|
||||
var existingIndex = await db.FileIndexes
|
||||
.Include(fi => fi.File)
|
||||
.FirstOrDefaultAsync(fi => fi.Id == indexId && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
var fileId = existingIndex.FileId;
|
||||
var fileName = existingIndex.File.Name;
|
||||
var filePath = existingIndex.Path;
|
||||
|
||||
// Remove the index
|
||||
var removed = await fileIndexService.RemoveAsync(indexId);
|
||||
|
||||
if (!removed)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
// Optionally delete the actual file
|
||||
if (!deleteFile)
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFile
|
||||
? "File index and file data removed successfully"
|
||||
: "File index removed successfully",
|
||||
FileId = fileId,
|
||||
FileName = fileName,
|
||||
Path = filePath,
|
||||
FileDataDeleted = deleteFile
|
||||
});
|
||||
try
|
||||
{
|
||||
// Check if there are any other indexes for this file
|
||||
var remainingIndexes = await fileIndexService.GetByFileIdAsync(fileId);
|
||||
if (remainingIndexes.Count == 0)
|
||||
{
|
||||
// No other indexes exist, safe to delete the file
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId.ToString());
|
||||
if (file != null)
|
||||
{
|
||||
db.Files.Remove(file);
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Deleted file {FileId} ({FileName}) as requested", fileId, fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to delete file {FileId} while removing index", fileId);
|
||||
// Continue even if file deletion fails
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFile
|
||||
? "File index and file data removed successfully"
|
||||
: "File index removed successfully",
|
||||
FileId = fileId,
|
||||
FileName = fileName,
|
||||
Path = filePath,
|
||||
FileDataDeleted = deleteFile
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to remove file index {IndexId} for account {AccountId}", indexId, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "REMOVE_FAILED",
|
||||
Message = "Failed to remove file",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all file indexes in a specific path
|
||||
/// </summary>
|
||||
/// <param name="path">The path to clear</param>
|
||||
/// <param name="deleteFiles">Whether to also delete the actual file data</param>
|
||||
/// <returns>Success message with count of removed items</returns>
|
||||
[HttpDelete("clear-path")]
|
||||
public async Task<IActionResult> ClearPath([FromQuery] string path = "/", [FromQuery] bool deleteFiles = false)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var removedCount = await fileIndexService.RemoveByPathAsync(accountId, path);
|
||||
|
||||
if (!deleteFiles || removedCount <= 0)
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFiles
|
||||
? $"Cleared {removedCount} file indexes from path and deleted orphaned files"
|
||||
: $"Cleared {removedCount} file indexes from path",
|
||||
Path = path,
|
||||
RemovedCount = removedCount,
|
||||
FilesDeleted = deleteFiles
|
||||
});
|
||||
// Get the files that were in this path and check if they have other indexes
|
||||
var filesInPath = await fileIndexService.GetByPathAsync(accountId, path);
|
||||
var fileIdsToCheck = filesInPath.Select(fi => fi.FileId).Distinct().ToList();
|
||||
|
||||
foreach (var fileId in fileIdsToCheck)
|
||||
{
|
||||
var remainingIndexes = await fileIndexService.GetByFileIdAsync(fileId);
|
||||
if (remainingIndexes.Count != 0) continue;
|
||||
// No other indexes exist, safe to delete the file
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId.ToString());
|
||||
if (file == null) continue;
|
||||
db.Files.Remove(file);
|
||||
logger.LogInformation("Deleted orphaned file {FileId} after clearing path {Path}", fileId, path);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFiles
|
||||
? $"Cleared {removedCount} file indexes from path and deleted orphaned files"
|
||||
: $"Cleared {removedCount} file indexes from path",
|
||||
Path = path,
|
||||
RemovedCount = removedCount,
|
||||
FilesDeleted = deleteFiles
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to clear path {Path} for account {AccountId}", path, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "CLEAR_PATH_FAILED",
|
||||
Message = "Failed to clear path",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new file index (useful for adding existing files to a path)
|
||||
/// </summary>
|
||||
/// <param name="request">The create index request</param>
|
||||
/// <returns>The created file index</returns>
|
||||
[HttpPost("create")]
|
||||
public async Task<IActionResult> CreateFileIndex([FromBody] CreateFileIndexRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify the file exists and belongs to the user
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == request.FileId);
|
||||
if (file == null)
|
||||
return new ObjectResult(ApiError.NotFound("File")) { StatusCode = 404 };
|
||||
|
||||
if (file.AccountId != accountId)
|
||||
return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 };
|
||||
|
||||
// Check if index already exists for this file and path
|
||||
var existingIndex = await db.FileIndexes
|
||||
.FirstOrDefaultAsync(fi =>
|
||||
fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex != null)
|
||||
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
||||
{
|
||||
{ "fileId", ["File index already exists for this path"] }
|
||||
})) { StatusCode = 400 };
|
||||
|
||||
var fileIndex = await fileIndexService.CreateAsync(request.Path, request.FileId, accountId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
IndexId = fileIndex.Id,
|
||||
fileIndex.FileId,
|
||||
fileIndex.Path,
|
||||
Message = "File index created successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create file index for file {FileId} at path {Path} for account {AccountId}",
|
||||
request.FileId, request.Path, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "CREATE_INDEX_FAILED",
|
||||
Message = "Failed to create file index",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for files by name or metadata
|
||||
/// </summary>
|
||||
/// <param name="query">The search query</param>
|
||||
/// <param name="path">Optional path to limit search to</param>
|
||||
/// <returns>Matching files</returns>
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> SearchFiles([FromQuery] string query, [FromQuery] string? path = null)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Build the query with all conditions at once
|
||||
var searchTerm = query.ToLower();
|
||||
var fileIndexes = await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.Where(fi =>
|
||||
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
||||
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
||||
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
||||
(fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm))))
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Query = query,
|
||||
Path = path,
|
||||
Results = fileIndexes,
|
||||
TotalCount = fileIndexes.Count()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to search files for account {AccountId} with query {Query}", accountId, query);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "SEARCH_FAILED",
|
||||
Message = "Failed to search files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MoveFileRequest
|
||||
{
|
||||
public string NewPath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class CreateFileIndexRequest
|
||||
{
|
||||
[MaxLength(32)] public string FileId { get; set; } = null!;
|
||||
public string Path { get; set; } = null!;
|
||||
}
|
||||
197
DysonNetwork.Drive/Index/FileIndexService.cs
Normal file
197
DysonNetwork.Drive/Index/FileIndexService.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Drive.Index;
|
||||
|
||||
public class FileIndexService(AppDatabase db)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new file index entry
|
||||
/// </summary>
|
||||
/// <param name="path">The parent folder path with a trailing slash</param>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <returns>The created file index</returns>
|
||||
public async Task<SnCloudFileIndex> CreateAsync(string path, string fileId, Guid accountId)
|
||||
{
|
||||
// Ensure a path has a trailing slash and is query-safe
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
// Check if a file with the same name already exists in the same path for this account
|
||||
var existingFileIndex = await db.FileIndexes
|
||||
.FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.Path == normalizedPath && fi.FileId == fileId);
|
||||
|
||||
if (existingFileIndex != null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A file with ID '{fileId}' already exists in path '{normalizedPath}' for account '{accountId}'");
|
||||
}
|
||||
|
||||
var fileIndex = new SnCloudFileIndex
|
||||
{
|
||||
Path = normalizedPath,
|
||||
FileId = fileId,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.FileIndexes.Add(fileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return fileIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing file index entry by removing the old one and creating a new one
|
||||
/// </summary>
|
||||
/// <param name="id">The file index ID</param>
|
||||
/// <param name="newPath">The new parent folder path with trailing slash</param>
|
||||
/// <returns>The updated file index</returns>
|
||||
public async Task<SnCloudFileIndex?> UpdateAsync(Guid id, string newPath)
|
||||
{
|
||||
var fileIndex = await db.FileIndexes.FindAsync(id);
|
||||
if (fileIndex == null)
|
||||
return null;
|
||||
|
||||
// Since properties are init-only, we need to remove the old index and create a new one
|
||||
db.FileIndexes.Remove(fileIndex);
|
||||
|
||||
var newFileIndex = new SnCloudFileIndex
|
||||
{
|
||||
Path = NormalizePath(newPath),
|
||||
FileId = fileIndex.FileId,
|
||||
AccountId = fileIndex.AccountId
|
||||
};
|
||||
|
||||
db.FileIndexes.Add(newFileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return newFileIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a file index entry by ID
|
||||
/// </summary>
|
||||
/// <param name="id">The file index ID</param>
|
||||
/// <returns>True if the index was found and removed, false otherwise</returns>
|
||||
public async Task<bool> RemoveAsync(Guid id)
|
||||
{
|
||||
var fileIndex = await db.FileIndexes.FindAsync(id);
|
||||
if (fileIndex == null)
|
||||
return false;
|
||||
|
||||
db.FileIndexes.Remove(fileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes file index entries by file ID
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <returns>The number of indexes removed</returns>
|
||||
public async Task<int> RemoveByFileIdAsync(string fileId)
|
||||
{
|
||||
var indexes = await db.FileIndexes
|
||||
.Where(fi => fi.FileId == fileId)
|
||||
.ToListAsync();
|
||||
|
||||
if (indexes.Count == 0)
|
||||
return 0;
|
||||
|
||||
db.FileIndexes.RemoveRange(indexes);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return indexes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes file index entries by account ID and path
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <param name="path">The parent folder path</param>
|
||||
/// <returns>The number of indexes removed</returns>
|
||||
public async Task<int> RemoveByPathAsync(Guid accountId, string path)
|
||||
{
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
var indexes = await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
||||
.ToListAsync();
|
||||
|
||||
if (!indexes.Any())
|
||||
return 0;
|
||||
|
||||
db.FileIndexes.RemoveRange(indexes);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return indexes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets file indexes by account ID and path
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <param name="path">The parent folder path</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path)
|
||||
{
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets file indexes by file ID
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByFileIdAsync(string fileId)
|
||||
{
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.FileId == fileId)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all file indexes for an account
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId)
|
||||
{
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path to ensure it has a trailing slash and is query-safe
|
||||
/// </summary>
|
||||
/// <param name="path">The original path</param>
|
||||
/// <returns>The normalized path</returns>
|
||||
public static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return "/";
|
||||
|
||||
// Ensure the path starts with a slash
|
||||
if (!path.StartsWith('/'))
|
||||
path = "/" + path;
|
||||
|
||||
// Ensure the path ends with a slash (unless it's just the root)
|
||||
if (path != "/" && !path.EndsWith('/'))
|
||||
path += "/";
|
||||
|
||||
// Make path query-safe by removing problematic characters
|
||||
// This is a basic implementation - you might want to add more robust validation
|
||||
path = path.Replace("%", "").Replace("'", "").Replace("\"", "");
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
341
DysonNetwork.Drive/Index/README.md
Normal file
341
DysonNetwork.Drive/Index/README.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# File Indexing System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The File Indexing System provides a hierarchical file organization layer on top of the existing file storage system in DysonNetwork Drive. It allows users to organize their files in folders and paths while maintaining the underlying file storage capabilities.
|
||||
|
||||
When using with the gateway, replace the `/api` with the `/drive` in the path.
|
||||
And all the arguments will be transformed into snake case via the gateway.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **SnCloudFileIndex Model** - Represents the file-to-path mapping
|
||||
2. **FileIndexService** - Business logic for file index operations
|
||||
3. **FileIndexController** - REST API endpoints for file management
|
||||
4. **FileUploadController Integration** - Automatic index creation during upload
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- File Indexes table
|
||||
CREATE TABLE "FileIndexes" (
|
||||
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"Path" character varying(8192) NOT NULL,
|
||||
"FileId" uuid NOT NULL,
|
||||
"AccountId" uuid NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
||||
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
||||
CONSTRAINT "PK_FileIndexes" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_FileIndexes_Files_FileId" FOREIGN KEY ("FileId") REFERENCES "Files" ("Id") ON DELETE CASCADE,
|
||||
INDEX "IX_FileIndexes_Path_AccountId" ("Path", "AccountId")
|
||||
);
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Browse Files
|
||||
**GET** `/api/index/browse?path=/documents/`
|
||||
|
||||
Browse files in a specific path.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional, default: "/") - The path to browse
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"path": "/documents/",
|
||||
"files": [
|
||||
{
|
||||
"id": "guid",
|
||||
"path": "/documents/",
|
||||
"fileId": "guid",
|
||||
"accountId": "guid",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
"file": {
|
||||
"id": "string",
|
||||
"name": "document.pdf",
|
||||
"size": 1024,
|
||||
"mimeType": "application/pdf",
|
||||
"hash": "sha256-hash",
|
||||
"uploadedAt": "2024-01-01T00:00:00Z",
|
||||
"expiredAt": null,
|
||||
"hasCompression": false,
|
||||
"hasThumbnail": true,
|
||||
"isEncrypted": false,
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Get All Files
|
||||
**GET** `/api/index/all`
|
||||
|
||||
Get all files for the current user across all paths.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
// Same structure as browse endpoint
|
||||
],
|
||||
"totalCount": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Move File
|
||||
**POST** `/api/index/move/{indexId}`
|
||||
|
||||
Move a file to a new path.
|
||||
|
||||
**Path Parameters:**
|
||||
- `indexId` - The file index ID
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"newPath": "/archived/"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"fileId": "guid",
|
||||
"indexId": "guid",
|
||||
"oldPath": "/documents/",
|
||||
"newPath": "/archived/",
|
||||
"message": "File moved successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Remove File Index
|
||||
**DELETE** `/api/index/remove/{indexId}?deleteFile=false`
|
||||
|
||||
Remove a file index. Optionally delete the actual file data.
|
||||
|
||||
**Path Parameters:**
|
||||
- `indexId` - The file index ID
|
||||
|
||||
**Query Parameters:**
|
||||
- `deleteFile` (optional, default: false) - Whether to also delete the file data
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "File index removed successfully",
|
||||
"fileId": "guid",
|
||||
"fileName": "document.pdf",
|
||||
"path": "/documents/",
|
||||
"fileDataDeleted": false
|
||||
}
|
||||
```
|
||||
|
||||
### Clear Path
|
||||
**DELETE** `/api/index/clear-path?path=/temp/&deleteFiles=false`
|
||||
|
||||
Remove all file indexes in a specific path.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional, default: "/") - The path to clear
|
||||
- `deleteFiles` (optional, default: false) - Whether to also delete orphaned files
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Cleared 5 file indexes from path",
|
||||
"path": "/temp/",
|
||||
"removedCount": 5,
|
||||
"filesDeleted": false
|
||||
}
|
||||
```
|
||||
|
||||
### Create File Index
|
||||
**POST** `/api/index/create`
|
||||
|
||||
Create a new file index for an existing file.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"fileId": "guid",
|
||||
"path": "/documents/"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"indexId": "guid",
|
||||
"fileId": "guid",
|
||||
"path": "/documents/",
|
||||
"message": "File index created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Search Files
|
||||
**GET** `/api/index/search?query=report&path=/documents/`
|
||||
|
||||
Search for files by name or metadata.
|
||||
|
||||
**Query Parameters:**
|
||||
- `query` (required) - The search query
|
||||
- `path` (optional) - Limit search to specific path
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"query": "report",
|
||||
"path": "/documents/",
|
||||
"results": [
|
||||
// Same structure as browse endpoint
|
||||
],
|
||||
"totalCount": 3
|
||||
}
|
||||
```
|
||||
|
||||
## Path Normalization
|
||||
|
||||
The system automatically normalizes paths to ensure consistency:
|
||||
|
||||
- **Trailing Slash**: All paths end with `/`
|
||||
- **Root Path**: User home folder is represented as `/`
|
||||
- **Query Safety**: Paths are validated to avoid SQL injection
|
||||
- **Examples**:
|
||||
- `/documents/` ✅ (correct)
|
||||
- `/documents` → `/documents/` ✅ (normalized)
|
||||
- `/documents/reports/` ✅ (correct)
|
||||
- `/documents/reports` → `/documents/reports/` ✅ (normalized)
|
||||
|
||||
## File Upload Integration
|
||||
|
||||
When uploading files with the `FileUploadController`, you can specify a path to automatically create file indexes:
|
||||
|
||||
**Create Upload Task Request:**
|
||||
```json
|
||||
{
|
||||
"fileName": "document.pdf",
|
||||
"fileSize": 1024,
|
||||
"contentType": "application/pdf",
|
||||
"hash": "sha256-hash",
|
||||
"path": "/documents/" // New field for file indexing
|
||||
}
|
||||
```
|
||||
|
||||
The system will automatically create a file index when the upload completes successfully.
|
||||
|
||||
## Service Methods
|
||||
|
||||
### FileIndexService
|
||||
|
||||
```csharp
|
||||
public class FileIndexService
|
||||
{
|
||||
// Create a new file index
|
||||
Task<SnCloudFileIndex> CreateAsync(string path, Guid fileId, Guid accountId);
|
||||
|
||||
// Get files by path
|
||||
Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path);
|
||||
|
||||
// Get all files for account
|
||||
Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId);
|
||||
|
||||
// Get indexes for specific file
|
||||
Task<List<SnCloudFileIndex>> GetByFileIdAsync(Guid fileId);
|
||||
|
||||
// Move file to new path
|
||||
Task<SnCloudFileIndex?> UpdateAsync(Guid indexId, string newPath);
|
||||
|
||||
// Remove file index
|
||||
Task<bool> RemoveAsync(Guid indexId);
|
||||
|
||||
// Remove all indexes in path
|
||||
Task<int> RemoveByPathAsync(Guid accountId, string path);
|
||||
|
||||
// Normalize path format
|
||||
public static string NormalizePath(string path);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API returns appropriate HTTP status codes and error messages:
|
||||
|
||||
- **400 Bad Request**: Invalid input parameters
|
||||
- **401 Unauthorized**: User not authenticated
|
||||
- **403 Forbidden**: User lacks permission
|
||||
- **404 Not Found**: Resource not found
|
||||
- **500 Internal Server Error**: Server-side error
|
||||
|
||||
**Error Response Format:**
|
||||
```json
|
||||
{
|
||||
"code": "BROWSE_FAILED",
|
||||
"message": "Failed to browse files",
|
||||
"status": 500
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Ownership Verification**: All operations verify that the user owns the file indexes
|
||||
2. **Path Validation**: Paths are normalized and validated
|
||||
3. **Cascade Deletion**: File indexes are automatically removed when files are deleted
|
||||
4. **Safe File Deletion**: Files are only deleted when no other indexes reference them
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Upload File to Specific Path
|
||||
```bash
|
||||
# Create upload task with path
|
||||
curl -X POST /api/files/upload/create \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"fileName": "report.pdf",
|
||||
"fileSize": 2048,
|
||||
"contentType": "application/pdf",
|
||||
"path": "/documents/reports/"
|
||||
}'
|
||||
```
|
||||
|
||||
### Browse Files
|
||||
```bash
|
||||
curl -X GET "/api/index/browse?path=/documents/reports/" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
### Move File
|
||||
```bash
|
||||
curl -X POST "/api/index/move/{indexId}" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"newPath": "/archived/"}'
|
||||
```
|
||||
|
||||
### Search Files
|
||||
```bash
|
||||
curl -X GET "/api/index/search?query=invoice&path=/documents/" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Trailing Slashes**: Always include trailing slashes in paths
|
||||
2. **Organize Hierarchically**: Use meaningful folder structures
|
||||
3. **Search Efficiently**: Use the search endpoint instead of client-side filtering
|
||||
4. **Clean Up**: Use the clear-path endpoint for temporary directories
|
||||
5. **Monitor Usage**: Check total file counts for quota management
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- The file indexing system works alongside the existing file storage
|
||||
- Files can exist in multiple paths (hard links)
|
||||
- File deletion is optional and only removes data when safe
|
||||
- The system maintains referential integrity between files and indexes
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
567
DysonNetwork.Drive/Migrations/20251108191230_AddPersistentTask.Designer.cs
generated
Normal file
567
DysonNetwork.Drive/Migrations/20251108191230_AddPersistentTask.Designer.cs
generated
Normal file
@@ -0,0 +1,567 @@
|
||||
// <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("20251108191230_AddPersistentTask")]
|
||||
partial class AddPersistentTask
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.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.CloudFileReference", 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 =>
|
||||
{
|
||||
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.Property<List<ContentSensitiveMark>>("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.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<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.CloudFileReference", 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.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.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersistentTask : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tasks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
task_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
progress = table.Column<double>(type: "double precision", nullable: false),
|
||||
parameters = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
|
||||
results = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
|
||||
error_message = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
started_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
completed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
last_activity = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
priority = table.Column<int>(type: "integer", nullable: false),
|
||||
estimated_duration_seconds = table.Column<long>(type: "bigint", nullable: true),
|
||||
discriminator = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false),
|
||||
file_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
file_size = table.Column<long>(type: "bigint", nullable: true),
|
||||
content_type = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
chunk_size = table.Column<long>(type: "bigint", nullable: true),
|
||||
chunks_count = table.Column<int>(type: "integer", nullable: true),
|
||||
chunks_uploaded = table.Column<int>(type: "integer", nullable: true),
|
||||
pool_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
bundle_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
encrypt_password = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
hash = table.Column<string>(type: "text", nullable: true),
|
||||
uploaded_chunks = table.Column<List<int>>(type: "integer[]", nullable: true),
|
||||
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_tasks", x => x.id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
632
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.Designer.cs
generated
Normal file
632
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.Designer.cs
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20251112135535_AddFileIndex")]
|
||||
partial class AddFileIndex
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.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.CloudFileReference", 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 =>
|
||||
{
|
||||
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.Property<List<ContentSensitiveMark>>("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.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.CloudFileReference", 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.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.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
66
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.cs
Normal file
66
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFileIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "path",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_indexes",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
path = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||
file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, 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_file_indexes", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_indexes_files_file_id",
|
||||
column: x => x.file_id,
|
||||
principalTable: "files",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_indexes_file_id",
|
||||
table: "file_indexes",
|
||||
column: "file_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_indexes_path_account_id",
|
||||
table: "file_indexes",
|
||||
columns: new[] { "path", "account_id" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_indexes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "path",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -21,7 +20,7 @@ namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -73,7 +72,224 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
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.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 =>
|
||||
{
|
||||
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)
|
||||
@@ -187,13 +403,17 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
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");
|
||||
@@ -202,42 +422,35 @@ namespace DysonNetwork.Drive.Migrations
|
||||
.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")
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
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");
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -296,86 +509,71 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
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("account_id");
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
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);
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -385,12 +583,43 @@ namespace DysonNetwork.Drive.Migrations
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
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.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user