REFACTOR: Optimized Catagory IDs INput Validation
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
name: Deploy Infrastructure
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "infrastructure/**"
|
||||
- ".gitea/workflows/infrastructure-deploy.yml"
|
||||
|
||||
# Pulumi state isn't safe to update concurrently; serialize runs and let a
|
||||
# newer push supersede one still queued (not one already applying).
|
||||
concurrency:
|
||||
group: pulumi-thamanyah-main
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
pulumi-up:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Broad infra-provisioning credentials, distinct from the narrowly
|
||||
# scoped gitea-ci-user used by the app deploy workflows (ECR push +
|
||||
# ecs:UpdateService only). Pulumi needs to create/update IAM, RDS,
|
||||
# ECS, CloudFront, etc., so this identity is intentionally wider.
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.PULUMI_AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.PULUMI_AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: infrastructure/go.mod
|
||||
|
||||
# Stack name assumes the runner is logged into the same Pulumi Cloud
|
||||
# org as `pulumi.yaml`'s default; if PULUMI_ACCESS_TOKEN's org differs,
|
||||
# qualify this as "<org>/main" instead.
|
||||
- name: Pulumi up
|
||||
uses: pulumi/actions@v6
|
||||
with:
|
||||
command: up
|
||||
stack-name: main
|
||||
work-dir: infrastructure
|
||||
env:
|
||||
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"thamanyah/cms/v2/internal/api"
|
||||
@@ -47,11 +48,6 @@ func ListCategories(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, api.CategoriesResponse{Categories: body})
|
||||
}
|
||||
|
||||
// allowedUploadContentTypes is the set of media types accepted for upload,
|
||||
// keyed by the exact string a client must send. Matched verbatim rather than
|
||||
// normalised: the value is signed into the presigned URL, so rewriting it
|
||||
// here would leave the client PUTting a header that no longer matches the
|
||||
// signature.
|
||||
var allowedUploadContentTypes = map[string]struct{}{
|
||||
"video/mp4": {}, // .mp4
|
||||
"video/quicktime": {}, // .mov
|
||||
@@ -147,18 +143,13 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
categoryIDs, unknown, err := resolveCategoryIDs(r.Context(), req.CategoryIDs)
|
||||
err := resolveCategoryIDs(r.Context(), req.CategoryIDs)
|
||||
if err != nil {
|
||||
log.Printf("Something Went Wrong Loading Categories: %s", err)
|
||||
writeProblem(w, http.StatusInternalServerError, "Something Went Wrong Loading Categories",
|
||||
"The submitted category ids could not be checked against the database, so no video record was created. This is a server-side fault; the uploaded file is still in storage, so retry this request with the same 'key'.")
|
||||
return
|
||||
}
|
||||
if len(unknown) > 0 {
|
||||
writeProblem(w, http.StatusUnprocessableEntity, "Unknown category.",
|
||||
fmt.Sprintf("The 'categoryIds' field referenced %s, which do not exist. Send ids taken from the 'id' field of GET /api/categories.", formatCategoryIDs(unknown)))
|
||||
return
|
||||
}
|
||||
|
||||
jobID, err := services.MediaConvertClient.QueueEncodingJob(r.Context(), key)
|
||||
if err != nil {
|
||||
@@ -171,7 +162,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
|
||||
video, err := services.DB.CreateVideo(r.Context(), services.Video{
|
||||
Title: title,
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
CategoryIDs: categoryIDs,
|
||||
CategoryIDs: req.CategoryIDs,
|
||||
Tags: strings.TrimSpace(req.Tags),
|
||||
FileName: strings.TrimSpace(req.FileName),
|
||||
StorageKey: key,
|
||||
@@ -202,40 +193,19 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// resolveCategoryIDs deduplicates the submitted category ids, preserving the
|
||||
// order they arrived in, and splits them into ids that exist and ids that do
|
||||
// not. Checking them here rather than leaning on the video_categories foreign
|
||||
// key keeps an unknown id from surfacing only after a transcoding job has
|
||||
// already been queued — and the duplicate pass keeps a repeated id from
|
||||
// tripping the join table's composite primary key. Callers reject an empty
|
||||
// list before calling, so a non-empty submitted list always yields either at
|
||||
// least one known id or at least one unknown one.
|
||||
func resolveCategoryIDs(ctx context.Context, submitted []int16) (known, unknown []int16, err error) {
|
||||
known = make([]int16, 0, len(submitted))
|
||||
|
||||
categories, err := services.DB.ListCategories(ctx)
|
||||
func resolveCategoryIDs(ctx context.Context, submitted []int16) (err error) {
|
||||
categoryIDS, err := services.DB.ListCategoriesIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
exists := make(map[int16]struct{}, len(categories))
|
||||
for _, category := range categories {
|
||||
exists[category.ID] = struct{}{}
|
||||
return err
|
||||
}
|
||||
|
||||
seen := make(map[int16]struct{}, len(submitted))
|
||||
for _, id := range submitted {
|
||||
if _, duplicate := seen[id]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
|
||||
if _, ok := exists[id]; ok {
|
||||
known = append(known, id)
|
||||
} else {
|
||||
unknown = append(unknown, id)
|
||||
for _, submittedID := range submitted {
|
||||
if !slices.Contains(categoryIDS, submittedID) {
|
||||
return fmt.Errorf("Incorrect Category IDs")
|
||||
}
|
||||
}
|
||||
return known, unknown, nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatCategoryIDs(ids []int16) string {
|
||||
|
||||
@@ -35,6 +35,7 @@ type Video struct {
|
||||
type DBClient interface {
|
||||
CreateVideo(ctx context.Context, v Video) (Video, error)
|
||||
ListCategories(ctx context.Context) ([]Category, error)
|
||||
ListCategoriesIDs(ctx context.Context) ([]int16, error)
|
||||
}
|
||||
|
||||
type DBConcrete struct {
|
||||
@@ -102,6 +103,24 @@ func (svc DBConcrete) ListCategories(ctx context.Context) ([]Category, error) {
|
||||
return categories, rows.Err()
|
||||
}
|
||||
|
||||
func (svc DBConcrete) ListCategoriesIDs(ctx context.Context) ([]int16, error) {
|
||||
rows, err := svc.SQLDB.QueryContext(ctx, `SELECT id FROM categories`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var categories []int16
|
||||
for rows.Next() {
|
||||
var category int16
|
||||
if err := rows.Scan(&category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
categories = append(categories, category)
|
||||
}
|
||||
return categories, rows.Err()
|
||||
}
|
||||
|
||||
func (svc DBConcrete) Migrate() {
|
||||
if err := db.Migrate(svc.SQLDB); err != nil {
|
||||
panic(err)
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user