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:
Ho Ngoc Hai
2026-01-13 22:29:50 +07:00
parent 56f08857db
commit 1bcdfcccac
2 changed files with 377 additions and 41 deletions

View File

@@ -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 |

View File

@@ -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)