mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
428 lines
11 KiB
Markdown
428 lines
11 KiB
Markdown
# Assets API
|
|
|
|
Base path: `/api/admin/assets`
|
|
Controller: `controllers/admin/assets.controller.js`
|
|
Storage: Chibisafe (CDN) + PostgreSQL via Sequelize
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
### Multer setup
|
|
|
|
The upload and update-thumbnail endpoints use `multer.fields()` — make sure your route file is configured with `memoryStorage`:
|
|
|
|
```js
|
|
const multer = require("multer");
|
|
const upload = multer({ storage: multer.memoryStorage() });
|
|
|
|
// Upload
|
|
router.post("/", upload.fields([{ name: "file", maxCount: 1 }, { name: "thumbnail", maxCount: 1 }]), assetsCtrl.uploadAsset);
|
|
|
|
// Update thumbnail
|
|
router.patch("/:assetId/thumbnail", upload.fields([{ name: "thumbnail", maxCount: 1 }]), assetsCtrl.updateThumbnail);
|
|
```
|
|
|
|
### Environment variables
|
|
|
|
```env
|
|
CHIBISAFE_BASE_URL=https://cdn.yourdomain.com
|
|
CHIBISAFE_API_KEY=your-api-key
|
|
|
|
CHIBISAFE_ALBUM_AVATARS=uuid
|
|
CHIBISAFE_ALBUM_VIDEOS=uuid
|
|
CHIBISAFE_ALBUM_DOCUMENTS=uuid
|
|
CHIBISAFE_ALBUM_THUMBNAILS=uuid
|
|
CHIBISAFE_ALBUM_ARCHIVED=uuid
|
|
```
|
|
|
|
### Album routing
|
|
|
|
`owner_type` is the single source of truth for which Chibisafe album a file lands in:
|
|
|
|
| `owner_type` | Chibisafe album | Intended use |
|
|
|---|---|---|
|
|
| `avatar` | avatars | Profile pictures |
|
|
| `video` | videos | Course / content videos |
|
|
| `document` | documents | PDF, DOCX, PPT, TXT, etc. |
|
|
| `thumbnail` | thumbnails | Set automatically — do not send manually |
|
|
| `image` | *(none)* | General-purpose images |
|
|
| anything else | *(none)* | Unclassified |
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
---
|
|
|
|
### GET `/`
|
|
|
|
List all assets (paginated).
|
|
|
|
**Query params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `page` | optional | Page number. Default: `1` |
|
|
| `limit` | optional | Items per page. Default: `10`, max: `1000` |
|
|
| `filters` | optional | JSON array of filter objects passed to `buildQuery` |
|
|
| `sort` | optional | JSON array of sort objects passed to `buildQuery` |
|
|
|
|
**Response `200`**
|
|
|
|
```json
|
|
{
|
|
"message": "Assets retrieved.",
|
|
"data": [...],
|
|
"pagination": {
|
|
"page": 1,
|
|
"limit": 10,
|
|
"totalRecords": 42,
|
|
"totalPages": 5,
|
|
"hasPrevPage": false,
|
|
"hasNextPage": true
|
|
},
|
|
"attributes": [...]
|
|
}
|
|
```
|
|
|
|
Soft-deleted assets are excluded automatically. Hidden fields (per `adminExclude`): `checksum`, `storage_bucket`, `storage_key`, `deletedBy`.
|
|
|
|
---
|
|
|
|
### GET `/:assetId`
|
|
|
|
Get a single asset by primary key.
|
|
|
|
**URL params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `assetId` | required | Asset primary key (BIGINT) |
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | `{ data: asset }` |
|
|
| `400` | Invalid asset ID |
|
|
| `404` | Asset not found |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
### POST `/`
|
|
|
|
Upload a new asset.
|
|
|
|
**Content-Type:** `multipart/form-data`
|
|
|
|
#### File fields
|
|
|
|
| Field | Required | Description |
|
|
|---|---|---|
|
|
| `file` | **required** | The main asset (image, video, document, etc.) |
|
|
| `thumbnail` | **required if video** | Cover image for the video. Ignored for non-video files. |
|
|
|
|
#### Text fields
|
|
|
|
| Field | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `uploadedBy` | **required** | — | User ID (BIGINT) of the uploader |
|
|
| `storage_provider` | **required** | — | `chibisafe` \| `local` \| `s3` \| `gcs` \| `cloudinary` |
|
|
| `owner_type` | optional | `null` | Determines album routing: `avatar`, `video`, `document`, `image` |
|
|
| `owner_id` | optional | `null` | ID of the owning entity (course ID, user ID, etc.) |
|
|
| `display_name` | optional | original filename | Human-readable name shown in the UI |
|
|
| `description` | optional | `null` | Free-text description |
|
|
| `is_public` | optional | `false` | `true` \| `false` |
|
|
| `access_level` | optional | `private` | `public` \| `private` \| `restricted` |
|
|
| `storage_bucket` | optional | `null` | Bucket name (S3 / GCS only) |
|
|
| `storage_key` | optional | `null` | Override storage key. Auto-set for Chibisafe (uses Chibisafe file UUID). |
|
|
| `file_url` | conditional | — | Required when `storage_provider` is not `local` or `chibisafe` |
|
|
| `width` | optional (non-video) | `null` | Image/document width in px. Ignored for videos. |
|
|
| `height` | optional (non-video) | `null` | Image/document height in px. Ignored for videos. |
|
|
|
|
#### Auto-extracted fields (videos only — do not send)
|
|
|
|
These are extracted server-side via **ffprobe** and will override anything the client sends:
|
|
|
|
| Field | Source | Example |
|
|
|---|---|---|
|
|
| `width` | ffprobe | `1920` |
|
|
| `height` | ffprobe | `1080` |
|
|
| `resolution` | derived | `1080p`, `720p`, `4K` |
|
|
| `duration` | ffprobe | `281.49` (seconds) |
|
|
| `frame_rate` | ffprobe | `23.976` (fps) |
|
|
| `bitrate` | ffprobe | `447933` (bps) |
|
|
| `video_codec` | ffprobe | `H.264`, `H.265`, `AV1`, `VP9` |
|
|
| `audio_codec` | ffprobe | `AAC`, `MP3`, `Opus` |
|
|
| `thumbnail_url` | Chibisafe upload | CDN URL of the uploaded thumbnail |
|
|
|
|
#### Transaction strategy
|
|
|
|
```
|
|
Phase 1 (no DB connection held — slow I/O):
|
|
├─ Validate inputs
|
|
├─ Upload main file to Chibisafe → track UUID for rollback
|
|
├─ Run ffprobe on video buffer → extract metadata
|
|
└─ Upload thumbnail to Chibisafe → track UUID for rollback
|
|
|
|
Phase 2 (transaction open ~milliseconds):
|
|
└─ Asset.create() → commit
|
|
|
|
On Phase 2 failure:
|
|
└─ rollback DB + deleteFile() all tracked Chibisafe UUIDs
|
|
```
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `201` | `{ data: asset }` — fully populated asset record |
|
|
| `400` | Missing `file`, `uploadedBy`, or `thumbnail` (for videos); buffer issues |
|
|
| `500` | DB or Chibisafe error — Chibisafe uploads are cleaned up automatically |
|
|
|
|
#### Example — video upload (Postman)
|
|
|
|
```
|
|
POST /api/admin/assets
|
|
Content-Type: multipart/form-data
|
|
|
|
file → (attach .mp4)
|
|
thumbnail → (attach .jpg)
|
|
uploadedBy → 1
|
|
storage_provider → chibisafe
|
|
owner_type → video
|
|
owner_id → 10
|
|
display_name → Intro to React
|
|
is_public → true
|
|
access_level → public
|
|
```
|
|
|
|
#### Example — avatar upload
|
|
|
|
```
|
|
POST /api/admin/assets
|
|
Content-Type: multipart/form-data
|
|
|
|
file → (attach .jpg)
|
|
uploadedBy → 1
|
|
storage_provider → chibisafe
|
|
owner_type → avatar
|
|
owner_id → 5
|
|
```
|
|
|
|
#### Example — document upload
|
|
|
|
```
|
|
POST /api/admin/assets
|
|
Content-Type: multipart/form-data
|
|
|
|
file → (attach .pdf)
|
|
uploadedBy → 1
|
|
storage_provider → chibisafe
|
|
owner_type → document
|
|
owner_id → 7
|
|
display_name → Module 1 Handout
|
|
```
|
|
|
|
---
|
|
|
|
### PATCH `/:assetId/thumbnail`
|
|
|
|
Replace the thumbnail image of an existing asset by uploading a new file.
|
|
|
|
**Content-Type:** `multipart/form-data`
|
|
|
|
**URL params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `assetId` | **required** | Asset primary key |
|
|
|
|
**File field**
|
|
|
|
| Field | Required | Description |
|
|
|---|---|---|
|
|
| `thumbnail` | **required** | New thumbnail image file |
|
|
|
|
**How it works**
|
|
|
|
1. Uploads the new thumbnail to Chibisafe (thumbnails album).
|
|
2. Updates `thumbnail_url` on the asset record.
|
|
3. Deletes the old thumbnail from Chibisafe (best-effort — non-fatal if it fails).
|
|
|
|
> **Note:** Old thumbnail cleanup requires a `thumbnail_storage_key` column on the Asset model to track the previous Chibisafe file UUID. Without it, the old thumbnail remains on Chibisafe but the DB record is updated correctly.
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | `{ data: asset }` — updated asset with new `thumbnail_url` |
|
|
| `400` | No thumbnail file attached |
|
|
| `404` | Asset not found |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
### PUT `/:assetId`
|
|
|
|
Update asset metadata. **File uploads are blocked on this endpoint.**
|
|
|
|
**Content-Type:** `application/json`
|
|
|
|
**URL params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `assetId` | **required** | Asset primary key |
|
|
|
|
**Body** — all fields optional, send only what changes
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `display_name` | string | New display name |
|
|
| `description` | string | New description |
|
|
| `owner_type` | string | New owner type |
|
|
| `owner_id` | number | New owner entity ID |
|
|
| `is_public` | boolean | `true` \| `false` |
|
|
| `access_level` | string | `public` \| `private` \| `restricted` |
|
|
| `thumbnail_url` | string | Manually replace thumbnail URL (use PATCH `/thumbnail` to upload a file instead) |
|
|
| `width` | number | Width in px. Re-derives `resolution` automatically. |
|
|
| `height` | number | Height in px. Re-derives `resolution` automatically. |
|
|
| `duration` | number | Duration in seconds |
|
|
| `frame_rate` | number | fps |
|
|
| `bitrate` | number | bps |
|
|
| `video_codec` | string | e.g. `H.264` |
|
|
| `audio_codec` | string | e.g. `AAC` |
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | `{ data: asset }` |
|
|
| `400` | Invalid ID or file attached to request |
|
|
| `404` | Asset not found |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
### DELETE `/:assetId`
|
|
|
|
Soft-delete a single asset.
|
|
|
|
Sets `deletedAt` on the DB record and moves the file to the **archived** album on Chibisafe (best-effort — non-fatal if Chibisafe is unavailable).
|
|
|
|
**Content-Type:** `application/json`
|
|
|
|
**URL params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `assetId` | **required** | Asset primary key |
|
|
|
|
**Body**
|
|
|
|
| Field | Required | Description |
|
|
|---|---|---|
|
|
| `deletedBy` | optional | User ID performing the delete |
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | Asset deleted |
|
|
| `400` | Invalid asset ID |
|
|
| `404` | Asset not found |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
### DELETE `/bulk`
|
|
|
|
Soft-delete multiple assets in one call.
|
|
|
|
All matching Chibisafe files are moved to the **archived** album in a single API call.
|
|
|
|
**Content-Type:** `application/json`
|
|
|
|
**Body**
|
|
|
|
| Field | Required | Description |
|
|
|---|---|---|
|
|
| `ids` | **required** | Non-empty array of asset IDs: `[1, 2, 3]` |
|
|
| `deletedBy` | optional | User ID performing the delete |
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | `N asset(s) deleted` |
|
|
| `400` | `ids` missing or empty |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
### POST `/:assetId/restore`
|
|
|
|
Restore a soft-deleted asset.
|
|
|
|
Clears `deletedAt` and `deletedBy` on the DB record, then moves the file on Chibisafe from the **archived** album back to its home album based on `owner_type`:
|
|
|
|
| `owner_type` | Moved back to |
|
|
|---|---|
|
|
| `video` | videos album |
|
|
| `avatar` | avatars album |
|
|
| `document` | documents album |
|
|
| `image` / anything else | no move (no dedicated album) |
|
|
|
|
The Chibisafe move is best-effort — a failed move will not block or roll back the DB restore.
|
|
|
|
**URL params**
|
|
|
|
| Param | Required | Description |
|
|
|---|---|---|
|
|
| `assetId` | **required** | Asset primary key (must be soft-deleted) |
|
|
|
|
**Body:** none required.
|
|
|
|
**Responses**
|
|
|
|
| Status | Description |
|
|
|---|---|
|
|
| `200` | `{ data: asset }` — Asset restored |
|
|
| `404` | Asset not found or not deleted |
|
|
| `500` | Internal server error |
|
|
|
|
---
|
|
|
|
## Response shape
|
|
|
|
All responses use `R.success` / `R.error` from `response.util`:
|
|
|
|
```json
|
|
// success
|
|
{
|
|
"message": "Asset uploaded.",
|
|
"data": { ... }
|
|
}
|
|
|
|
// error
|
|
{
|
|
"message": "Asset not found.",
|
|
"status": 404
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Related files
|
|
|
|
| File | Purpose |
|
|
|---|---|
|
|
| `models/assets/assets.mdl.js` | Sequelize model |
|
|
| `models/assets/assets.attributes.js` | Exclude sets, paginate config |
|
|
| `services/chibisafe.service.js` | Chibisafe API wrapper (upload, delete, archive, album) |
|
|
| `services/ffprobe.service.js` | ffprobe metadata extraction for videos |
|
|
| `utils/paginate.util.js` | Paginated `findAndCountAll` used by `getAssets` |
|
|
| `utils/response.util.js` | `R.success` / `R.error` response helpers | |