Files
classroomio/.cursor/plans/global_presigned_url_strategy_ac12cdab.plan.md
T

157 lines
9.4 KiB
Markdown
Raw Normal View History

2026-03-09 11:18:22 +01:00
---
name: Global presigned URL strategy
overview: Add Redis caching for presigned download URLs on the API, and introduce a PresignApi class on the frontend so all presign requests go through one API class instead of calling classroomio.course.presign directly.
todos: []
isProject: false
---
# Global Presigned URL Strategy – Redis Cache
## Current state
Every place that needs a presigned download URL calls S3 `getSignedUrl` with no caching:
| Caller | When | Goes through |
| ----------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [lesson-media.ts](apps/api/src/utils/lesson-media.ts) | Enrich single lesson (documents/videos) | `generateDocumentDownloadPresignedUrls` / `generateVideoDownloadPresignedUrls` |
| [presign.ts](apps/api/src/routes/course/presign.ts) | Client calls `POST /presign/document/download` or `/video/download` | Same S3 utils |
| Client `enrichFileUploadAnswersWithUrls` | Open exercise submission | Client → `POST presign/document/download` |
**Problem**: Same fileKey generates a new presigned URL every time—whether loading a lesson again, opening the grading modal again, or re-requesting media. Server-side enrichment for lessons does not cache; each lesson load hits S3 again.
---
## Proposed approach: Redis cache in API
Cache presigned URLs in Redis at the S3 utils layer so all callers benefit without changes.
```mermaid
flowchart TB
subgraph callers [Callers]
LessonMedia[lesson-media.ts]
PresignRoute[presign routes]
Future[future submission enrichment]
end
subgraph s3Utils [apps/api/src/utils/s3.ts]
GenUrls[generateDownloadPresignedUrls]
end
subgraph cache [Redis Cache]
Check["GET presign:download:{bucket}:{key}"]
Store["SETEX TTL 50min"]
end
LessonMedia --> GenUrls
PresignRoute --> GenUrls
Future --> GenUrls
GenUrls --> Check
Check -->|"hit"| Return[Return cached URL]
Check -->|"miss"| S3[getSignedUrl from AWS SDK]
S3 --> Store
Store --> Return
```
---
## Part A: Redis cache (API)
### 1. Add Redis cache inside presigned URL logic
**File**: [apps/api/src/utils/s3.ts](apps/api/src/utils/s3.ts)
- Import `redis` from [redis/redis.ts](apps/api/src/utils/redis/redis.ts)
- Inside `generateDownloadPresignedUrls`:
- Cache key: `presign:download:{bucketName}:{fileKey}` (bucket distinguishes documents vs videos)
- TTL: 3000 seconds (50 min); presigned URLs are typically valid 1 hour
- Use `redis.mget()` for batch lookup of cache keys before generating
- For cache hits: add to result
- For misses: generate via `getSignedUrl`, store with `redis.setex()`, add to result
- `generateVideoDownloadPresignedUrls` and `generateDocumentDownloadPresignedUrls` call `generateDownloadPresignedUrls`, so they automatically use the cache; no changes needed
### 2. Graceful Redis failure
- If Redis is down or `mget`/`setex` fails, fall back to generating URLs without cache (current behavior)
- Log the error but do not fail the request
---
## Part B: PresignApi class (frontend)
Create a single API class responsible for all presigned URL requests so components never call `classroomio.course.presign...` directly.
### 1. PresignApi class
**New file**: [apps/dashboard/src/lib/features/course/api/presign.svelte.ts](apps/dashboard/src/lib/features/course/api/presign.svelte.ts)
- Extend `BaseApiWithErrors`, use `this.execute<RequestType>()` per AGENTS.md
- Methods:
- `getDocumentUploadUrl(fileName: string, fileType: string)` → `{ url, fileKey }`
- `getDocumentDownloadUrls(keys: string[])` → `Record<string, string>`
- `getVideoUploadUrl(fileName: string, fileType: string)` → `{ url, fileKey }`
- `getVideoDownloadUrls(keys: string[])` → `Record<string, string>`
- Each method wraps `classroomio.course.presign.document.upload`, `document.download`, `video.upload`, or `video.download`
- Export singleton: `export const presignApi = new PresignApi()`
### 2. Types in `utils/types.ts`
**File**: [apps/dashboard/src/lib/features/course/utils/types.ts](apps/dashboard/src/lib/features/course/utils/types.ts)
- Add request types inferred from API: `DocumentUploadPresignRequest`, `DocumentDownloadPresignRequest`, etc.
### 3. Export from course api
**File**: [apps/dashboard/src/lib/features/course/api/index.ts](apps/dashboard/src/lib/features/course/api/index.ts)
- Export `presignApi` from presign.svelte.ts
### 4. Callers to update
| Caller | Current | Change |
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| [view-mode.svelte](apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte) | `classroomio.course.presign.document.upload.$post` and `document.download.$post` in `handleFileUpload` | Use `presignApi.getDocumentUploadUrl` and `presignApi.getDocumentDownloadUrls`; remove `classroomio` import |
| [functions.ts](apps/dashboard/src/lib/features/course/utils/functions.ts) `enrichFileUploadAnswersWithUrls` | `classroomio.course.presign.document.download.$post` | Use `presignApi.getDocumentDownloadUrls` (needs access to presignApi singleton) |
| [media-manager.svelte.ts](apps/dashboard/src/lib/features/media/api/media-manager.svelte.ts) `getAssetDownloadUrl` | `classroomio.course.presign.document.download` or `video.download` | Use `presignApi.getDocumentDownloadUrls` or `presignApi.getVideoDownloadUrls` |
| [presign.ts](apps/dashboard/src/lib/utils/services/courses/presign.ts) `GenericUploader` | Calls `classroomio.course.presign...` in `getPresignedUrl` and `getDownloadPresignedUrl` | Use `presignApi` methods instead |
**Note**: `enrichFileUploadAnswersWithUrls` is a pure function in `functions.ts`. It would need to either:
- Accept `presignApi` (or a fetch function) as a parameter, or
- Import `presignApi` directly from `$features/course/api`
The latter is simpler; we import presignApi where needed.
---
## Files to change
| File | Change |
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [apps/api/src/utils/s3.ts](apps/api/src/utils/s3.ts) | Add Redis cache check/store inside `generateDownloadPresignedUrls` |
| [apps/dashboard/src/lib/features/course/api/presign.svelte.ts](apps/dashboard/src/lib/features/course/api/presign.svelte.ts) | New: PresignApi class |
| [apps/dashboard/src/lib/features/course/utils/types.ts](apps/dashboard/src/lib/features/course/utils/types.ts) | Add presign request types |
| [apps/dashboard/src/lib/features/course/api/index.ts](apps/dashboard/src/lib/features/course/api/index.ts) | Export presignApi |
| [view-mode.svelte](apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte) | Use presignApi; remove classroomio import |
| [functions.ts](apps/dashboard/src/lib/features/course/utils/functions.ts) | Use presignApi.getDocumentDownloadUrls |
| [media-manager.svelte.ts](apps/dashboard/src/lib/features/media/api/media-manager.svelte.ts) | Use presignApi for download URLs |
| [presign.ts](apps/dashboard/src/lib/utils/services/courses/presign.ts) | GenericUploader uses presignApi |
---
## Benefits
- **Lesson documents/videos**: Re-loading the same lesson uses cached URLs
- **Presign API**: Repeated client calls for same keys hit cache
- **Exercise submissions**: When client calls `enrichFileUploadAnswersWithUrls` (or future server enrichment), cache reduces S3 calls
- **Media manager**: Same pattern—shared cache across the app