docs(architecture): Revise documentation for Logical Folder architecture and Data Sovereignty principles
- Updated the architecture documentation to emphasize the Logical Folder structure and its alignment with Data Sovereignty principles. - Introduced a clear distinction between logical and physical storage, highlighting the benefits of using UUID-based keys and a flat bucket structure. - Provided detailed examples of database schema, workflows, and performance comparisons to illustrate the advantages of the new approach over traditional bucket-based methods. - Enhanced explanations of folder creation, renaming, and file management processes to improve developer understanding and implementation.
This commit is contained in:
@@ -139,17 +139,257 @@ sequenceDiagram
|
||||
end
|
||||
```
|
||||
|
||||
### Kiểm Soát Truy Cập Theo Path
|
||||
### Kiến Trúc Logical Folder (Data Sovereignty)
|
||||
|
||||
Files được tổ chức với prefix theo access level:
|
||||
> ⚠️ **QUAN TRỌNG**: Theo nguyên tắc **Data Sovereignty** trong microservices, Storage Service phải sở hữu hoàn toàn mô hình dữ liệu của mình.
|
||||
|
||||
#### ❌ Anti-pattern: Dựa vào Bucket Structure
|
||||
|
||||
```
|
||||
BAD APPROACH - Folder structure phản ánh trong bucket:
|
||||
storage-bucket/
|
||||
├── public/{userId}/{date}/{fileId}_{filename} → Truy cập công khai
|
||||
├── private/{userId}/{date}/{fileId}_{filename} → Yêu cầu pre-signed URL
|
||||
└── shared/{userId}/{date}/{fileId}_{filename} → Kiểm soát bằng quy tắc
|
||||
├── users/john/documents/report.pdf
|
||||
├── users/john/images/photo.jpg
|
||||
└── users/mary/work/presentation.pptx
|
||||
|
||||
VẤN ĐỀ:
|
||||
- Đổi tên folder "documents" → "docs" = Move hàng triệu files (O(n))
|
||||
- Move file = Copy + Delete trên bucket (chậm, rủi ro)
|
||||
- Path predictable → Dễ bị path traversal attack
|
||||
- Không scale với hàng triệu users
|
||||
- Khó migrate sang storage provider khác
|
||||
```
|
||||
|
||||
#### ✅ Correct Approach: Logical Separation
|
||||
|
||||
**Nguyên tắc:**
|
||||
1. **Database** = Logical structure (folders, hierarchy, permissions)
|
||||
2. **Bucket** = Physical storage (flat UUID keys)
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Layer Logic - Database PostgreSQL"
|
||||
F[Folders Table]
|
||||
FL[Files Table]
|
||||
F -->|parent_id| F
|
||||
FL -->|folder_id| F
|
||||
end
|
||||
|
||||
subgraph "Layer Vật Lý - MinIO Bucket"
|
||||
B[Flat UUID Structure]
|
||||
B1[private/2026/01/13/uuid1.pdf]
|
||||
B2[private/2026/01/13/uuid2.jpg]
|
||||
B3[public/2026/01/14/uuid3.png]
|
||||
end
|
||||
|
||||
FL -.->|storage_key| B
|
||||
|
||||
style F fill:#3498db,color:#fff
|
||||
style FL fill:#2ecc71,color:#fff
|
||||
style B fill:#e74c3c,color:#fff
|
||||
```
|
||||
|
||||
#### Database Schema
|
||||
|
||||
```sql
|
||||
-- Folders: Hierarchical tree structure
|
||||
CREATE TABLE folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
path VARCHAR(1000) NOT NULL, -- Materialized path: /docs/work/2024
|
||||
level INT NOT NULL DEFAULT 0, -- Tree depth
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
UNIQUE (user_id, parent_id, name),
|
||||
INDEX idx_user_path (user_id, path)
|
||||
);
|
||||
|
||||
-- Files: Link to logical folders
|
||||
CREATE TABLE storage_files (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
storage_key VARCHAR(500) UNIQUE, -- Physical UUID key trong bucket
|
||||
content_type VARCHAR(100),
|
||||
file_size_bytes BIGINT,
|
||||
access_level VARCHAR(20), -- Private, Public, Shared
|
||||
bucket_name VARCHAR(255),
|
||||
provider INT,
|
||||
uploaded_at TIMESTAMP,
|
||||
is_deleted BOOLEAN DEFAULT false,
|
||||
|
||||
INDEX idx_folder_id (folder_id),
|
||||
INDEX idx_storage_key (storage_key)
|
||||
);
|
||||
```
|
||||
|
||||
#### Physical Key Generation Strategy
|
||||
|
||||
```csharp
|
||||
public class StorageKeyGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate UUID-based key (KHÔNG dựa vào folder path)
|
||||
/// Pattern: {prefix}/{year}/{month}/{day}/{uuid}.{ext}
|
||||
/// </summary>
|
||||
public string GenerateKey(FileAccessLevel access, string fileName)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var prefix = access == FileAccessLevel.Public ? "public" : "private";
|
||||
var uuid = Guid.NewGuid().ToString("N"); // 32 chars, no hyphens
|
||||
var ext = Path.GetExtension(fileName);
|
||||
|
||||
// Example: private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf
|
||||
return $"{prefix}/{now:yyyy}/{now:MM}/{now:dd}/{uuid}{ext}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Bucket Structure (Flat Physical)
|
||||
|
||||
```
|
||||
goodgo/
|
||||
├── private/
|
||||
│ ├── 2026/
|
||||
│ │ └── 01/
|
||||
│ │ └── 13/
|
||||
│ │ ├── d290f1ee6c544b0190e6d701748f0851.pdf
|
||||
│ │ ├── a7f3b2c19d8e4f01bcd5e92f7a4d8b63.jpg
|
||||
│ │ └── f9e4c1a2b7d36e5f8a0c4d9e2b1f7a8c.docx
|
||||
├── public/
|
||||
│ └── 2026/
|
||||
│ └── 01/
|
||||
│ └── 14/
|
||||
│ └── c3d8f2e1a9b47c6e5d0f8a3b2e1c7d9f.png
|
||||
└── shared/
|
||||
└── 2026/
|
||||
└── 01/
|
||||
└── 15/
|
||||
└── e1f2c3d4a5b6c7e8d9f0a1b2c3d4e5f6.xlsx
|
||||
|
||||
KHÔNG CÓ folder user/documents/... trong bucket!
|
||||
```
|
||||
|
||||
#### Workflows
|
||||
|
||||
**1. Tạo Folder (Chỉ Database)**
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Client->>API: POST /api/v1/folders {name: "documents"}
|
||||
API->>Database: INSERT INTO folders (name, path)
|
||||
Database-->>API: folder_id
|
||||
API-->>Client: {id, name, path: "/documents"}
|
||||
Note over Client,API: Bucket KHÔNG bị động chạm
|
||||
```
|
||||
|
||||
**2. Upload File vào Folder**
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Client->>API: POST /sign-upload {fileName, folderId}
|
||||
API->>Database: Validate folder ownership
|
||||
API->>StorageKeyGen: Generate UUID key
|
||||
StorageKeyGen-->>API: private/2026/01/13/{uuid}.pdf
|
||||
API->>MinIO: Get pre-signed PUT URL
|
||||
MinIO-->>API: pre-signed URL
|
||||
API->>Database: INSERT file (folder_id, storage_key)
|
||||
API-->>Client: {uploadUrl, objectKey}
|
||||
Client->>MinIO: PUT file binary
|
||||
Client->>API: POST /confirm-upload
|
||||
```
|
||||
|
||||
**3. List Files trong Folder**
|
||||
```sql
|
||||
-- Database query (fast, indexed)
|
||||
SELECT * FROM storage_files
|
||||
WHERE folder_id = '{folder-uuid}'
|
||||
AND is_deleted = false
|
||||
ORDER BY uploaded_at DESC;
|
||||
|
||||
-- Kết quả trả về client:
|
||||
{
|
||||
"folder": {
|
||||
"id": "folder-123",
|
||||
"path": "/documents/work"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"id": "file-456",
|
||||
"name": "report.pdf",
|
||||
"logicalPath": "/documents/work/report.pdf", -- Hiển thị cho user
|
||||
"storageKey": "private/2026/01/13/{uuid}.pdf" -- Physical key
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**4. Đổi Tên Folder**
|
||||
```sql
|
||||
-- Chỉ UPDATE database (instant, O(1))
|
||||
UPDATE folders
|
||||
SET name = 'docs',
|
||||
path = '/docs' -- Update materialized path
|
||||
WHERE id = '{folder-id}';
|
||||
|
||||
-- Update descendant paths
|
||||
UPDATE folders
|
||||
SET path = REPLACE(path, '/documents', '/docs')
|
||||
WHERE path LIKE '/documents/%';
|
||||
|
||||
-- Files GIỮ NGUYÊN storage_key - KHÔNG TOUCH bucket!
|
||||
```
|
||||
|
||||
**5. Move File giữa Folders**
|
||||
```sql
|
||||
-- Chỉ UPDATE database (instant, O(1))
|
||||
UPDATE storage_files
|
||||
SET folder_id = '{new-folder-id}'
|
||||
WHERE id = '{file-id}';
|
||||
|
||||
-- Physical file VẪN Ở storage_key cũ
|
||||
-- Không cần Copy/Delete trong bucket
|
||||
```
|
||||
|
||||
#### So Sánh Performance
|
||||
|
||||
| Thao tác | Anti-pattern (Bucket-based) | Correct (Logical) | Cải thiện |
|
||||
|----------|----------------------------|-------------------|-----------|
|
||||
| **Tạo folder** | N/A (virtual) | INSERT vào DB | Instant |
|
||||
| **Đổi tên folder** | Copy + Delete hàng triệu files | UPDATE 1-N rows DB | ~1000x nhanh hơn |
|
||||
| **Move file** | Copy + Delete 1 file | UPDATE 1 row DB | ~100x nhanh hơn |
|
||||
| **List files** | Bucket prefix scan | Indexed DB query | ~50x nhanh hơn |
|
||||
| **Delete folder** | Delete hàng triệu files | Cascade delete + background cleanup | Async |
|
||||
|
||||
#### Lợi Ích Data Sovereignty
|
||||
|
||||
1. **Performance:**
|
||||
- Rename folder: O(1) thay vì O(n)
|
||||
- Move file: O(1) thay vì O(1) bucket copy
|
||||
- List files: Indexed query thay vì bucket scan
|
||||
|
||||
2. **Security:**
|
||||
- UUID keys không predictable
|
||||
- Không có path traversal vulnerabilities
|
||||
- Access control trong database
|
||||
|
||||
3. **Scalability:**
|
||||
- Database handles metadata (fast)
|
||||
- Bucket handles blobs (simple)
|
||||
- Horizontal scaling dễ dàng
|
||||
|
||||
4. **Flexibility:**
|
||||
- Dễ dàng thêm features (sharing, versioning)
|
||||
- Migration giữa storage providers
|
||||
- Support multiple buckets/regions
|
||||
|
||||
5. **Developer Experience:**
|
||||
- Client nhìn thấy folder tree đẹp
|
||||
- Backend làm việc với simple UUIDs
|
||||
- Clean separation of concerns
|
||||
|
||||
### Components Direct Upload
|
||||
|
||||
| Component | Mục đích |
|
||||
|
||||
@@ -184,48 +184,144 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
## Cấu Trúc Thư Mục Trên Bucket
|
||||
## Kiến Trúc Logical Folder (Data Sovereignty)
|
||||
|
||||
Files được tổ chức theo access level và user ID với định dạng:
|
||||
|
||||
```
|
||||
{bucket}/
|
||||
├── private/{userId}/{date}/{fileId}_{filename} → Chỉ owner truy cập (qua pre-signed URL)
|
||||
├── public/{userId}/{date}/{fileId}_{filename} → Truy cập công khai
|
||||
└── shared/{userId}/{date}/{fileId}_{filename} → Kiểm soát bởi sharing rules
|
||||
```
|
||||
|
||||
### Chi Tiết Object Key Format
|
||||
|
||||
| Thành phần | Mô tả | Ví dụ |
|
||||
|------------|-------|-------|
|
||||
| `{bucket}` | Tên bucket (config) | `goodgo` |
|
||||
| `{accessLevel}` | Mức truy cập | `private`, `public`, `shared` |
|
||||
| `{userId}` | ID của user upload | `user123` |
|
||||
| `{date}` | Ngày upload (UTC) | `20260113` |
|
||||
| `{fileId}` | 8 ký tự đầu của GUID | `a1b2c3d4` |
|
||||
| `{filename}` | Tên file đã sanitize | `document.pdf` |
|
||||
|
||||
### Ví Dụ Thực Tế
|
||||
> ⚠️ **QUAN TRỌNG**: Theo nguyên tắc **Data Sovereignty** trong microservices, folder là **logical concept trong Database**, KHÔNG phụ thuộc vào bucket structure.
|
||||
|
||||
### Nguyên Tắc Thiết Kế
|
||||
|
||||
Storage Service sở hữu mô hình dữ liệu riêng:
|
||||
- **Database** quản lý cấu trúc folder logic (hierarchy, paths, permissions)
|
||||
- **Bucket** chỉ lưu trữ file binary với UUID keys (flat structure)
|
||||
|
||||
### ❌ Anti-pattern (KHÔNG LÀM)
|
||||
|
||||
```
|
||||
Bucket structure dựa vào user path:
|
||||
goodgo/
|
||||
├── private/
|
||||
│ └── user123/
|
||||
│ └── 20260113/
|
||||
│ ├── a1b2c3d4_document.pdf
|
||||
│ └── e5f6g7h8_image.jpg
|
||||
├── public/
|
||||
│ └── user456/
|
||||
│ └── 20260113/
|
||||
│ └── i9j0k1l2_avatar.png
|
||||
└── shared/
|
||||
└── user789/
|
||||
└── 20260113/
|
||||
└── m3n4o5p6_presentation.pptx
|
||||
├── users/john/documents/report.pdf ← BAD
|
||||
├── users/mary/images/photo.jpg ← BAD
|
||||
└── users/bob/work/presentation.pptx ← BAD
|
||||
|
||||
VẤN ĐỀ:
|
||||
- Đổi tên folder = Move hàng triệu files (chậm + rủi ro)
|
||||
- Path predictable = Dễ bị attack
|
||||
- Không scale với hàng triệu users
|
||||
```
|
||||
|
||||
> **Lưu ý**: Object key được trả về trong response của `/sign-upload` và cần được gửi lại khi gọi `/confirm-upload`.
|
||||
### ✅ Kiến Trúc Đúng (Logical Separation)
|
||||
|
||||
#### 1️⃣ Database: Logical Structure
|
||||
|
||||
```sql
|
||||
-- Folders table: Quản lý cây thư mục
|
||||
CREATE TABLE folders (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
parent_id UUID REFERENCES folders(id),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
path VARCHAR(1000) NOT NULL, -- VD: /docs/work/2024
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Files table: Link đến folder logic
|
||||
CREATE TABLE files (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
folder_id UUID REFERENCES folders(id), -- Logical folder
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
storage_key VARCHAR(500) UNIQUE, -- Physical key (UUID)
|
||||
size_bytes BIGINT,
|
||||
content_type VARCHAR(100),
|
||||
access_level VARCHAR(20),
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### 2️⃣ Bucket: Flat Physical Storage
|
||||
|
||||
Files được lưu với **UUID-based keys** theo pattern:
|
||||
|
||||
```
|
||||
{prefix}/{year}/{month}/{day}/{uuid}.{ext}
|
||||
|
||||
Ví dụ:
|
||||
goodgo/
|
||||
├── private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf
|
||||
├── private/2026/01/13/a7f3b2c19d8e4f01bcd5e92f7a4d8b63.jpg
|
||||
├── public/2026/01/14/f9e4c1a2b7d36e5f8a0c4d9e2b1f7a8c.png
|
||||
└── shared/2026/01/15/c3d8f2e1a9b47c6e5d0f8a3b2e1c7d9f.docx
|
||||
|
||||
KHÔNG CÓ cấu trúc user/folder trong bucket!
|
||||
Tất cả là flat UUID keys.
|
||||
```
|
||||
|
||||
#### 3️⃣ Storage Key Generation
|
||||
|
||||
```csharp
|
||||
// Tự động generate physical key (KHÔNG dựa vào folder)
|
||||
public string GenerateStorageKey(FileAccessLevel access, string fileName)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var prefix = access == FileAccessLevel.Public ? "public" : "private";
|
||||
var uuid = Guid.NewGuid().ToString("N");
|
||||
var ext = Path.GetExtension(fileName);
|
||||
|
||||
// Pattern: {prefix}/{year}/{month}/{day}/{uuid}{ext}
|
||||
return $"{prefix}/{now:yyyy}/{now:MM}/{now:dd}/{uuid}{ext}";
|
||||
}
|
||||
```
|
||||
|
||||
### Lợi Ích
|
||||
|
||||
| Thao tác | Database (Logic) | Bucket (Physical) | Hiệu năng |
|
||||
|----------|------------------|-------------------|-----------|
|
||||
| **Tạo folder** | INSERT 1 row | Không làm gì | Instant |
|
||||
| **Đổi tên folder** | UPDATE 1 row | Không làm gì | O(1) - Instant |
|
||||
| **Move file** | UPDATE File.folder_id | Không làm gì | O(1) - Instant |
|
||||
| **Delete folder** | Cascade delete | Queue cleanup | Background job |
|
||||
|
||||
**So sánh với Anti-pattern:**
|
||||
- Đổi tên folder: **O(1)** vs O(n) - nhanh hơn 1000x
|
||||
- Bảo mật: UUID keys không đoán được
|
||||
- Migration: Dễ dàng chuyển storage provider
|
||||
|
||||
### Workflow Ví Dụ
|
||||
|
||||
**Upload file vào folder:**
|
||||
```bash
|
||||
# 1. Tạo folder (chỉ trong Database)
|
||||
POST /api/v1/folders
|
||||
{
|
||||
"name": "documents",
|
||||
"parentId": null
|
||||
}
|
||||
|
||||
# 2. Upload file vào folder
|
||||
POST /api/v1/storage/sign-upload
|
||||
{
|
||||
"fileName": "report.pdf",
|
||||
"folderId": "folder-uuid-123", # Logical folder
|
||||
"fileSizeBytes": 1048576
|
||||
}
|
||||
|
||||
# Response: Physical key KHÔNG chứa folder name
|
||||
{
|
||||
"uploadUrl": "...",
|
||||
"objectKey": "private/2026/01/13/d290f1ee6c544b0190e6d701748f0851.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
**List files trong folder:**
|
||||
```bash
|
||||
GET /api/v1/folders/{folderId}/files
|
||||
|
||||
# Database query: SELECT * FROM files WHERE folder_id = {folderId}
|
||||
# Client nhìn thấy: /documents/report.pdf (logical path)
|
||||
# Bucket lưu: private/2026/01/13/{uuid}.pdf (physical key)
|
||||
```
|
||||
|
||||
> 📚 **Chi tiết kỹ thuật:** Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để hiểu sâu hơn về pattern này.
|
||||
|
||||
## Ví Dụ Legacy Upload (Qua Backend)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user