mobile v0.1 with deployment for ios (#191)

* stage 1: project init

* stage 2: skeleton with navigation

* step 2.5: streams list

* step 4: stream playback experience

* step 5-6: compose experience

* fix: broken record

* transcode media particles to mp4

* build: reproducible go generate

* build: rename skaffold module for particle processor worker

* infra: increase particle processor worker resources

Was dealing with OOM errors

* tweaks to mobile

* log transcode work

* view on desktop placeholder

* tweak padding

* cap video resolution to save on memory

* infra: bump memory limits as insurance

* ux improvements

* update bundle id for mobile

* config for mobile
This commit was merged in pull request #191.
This commit is contained in:
Arjun Patel
2026-04-29 17:39:11 -07:00
committed by GitHub
parent 3a11a82cd3
commit e3461dd5cd
110 changed files with 14682 additions and 22 deletions
+9
View File
@@ -29,6 +29,15 @@ type PrepareUploadResult struct {
UploadHeaders map[string]string
}
// CreateFromReaderInput is for server-side direct uploads (no presigned URL).
// Used by background workers that already have the bytes on hand and don't
// need a client round-trip.
type CreateFromReaderInput struct {
Prefix string // Optional prefix for organizing objects (e.g., network_id)
Name string
ContentType string
}
// Config holds configuration for the depot service
type Config struct {
GoogleServiceAccountEmail string
+52
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"time"
@@ -20,6 +21,7 @@ const (
type Service interface {
PrepareUpload(ctx context.Context, input PrepareUploadInput) (*PrepareUploadResult, error)
ConfirmUpload(ctx context.Context, objectID string) (*Object, error)
CreateFromReader(ctx context.Context, input CreateFromReaderInput, body io.Reader) (*Object, error)
GetByID(ctx context.Context, objectID string) (*Object, error)
GetDownloadURL(ctx context.Context, objectID string) (string, error)
Delete(ctx context.Context, objectID string) error
@@ -155,6 +157,56 @@ func (s *serviceImpl) ConfirmUpload(ctx context.Context, objectID string) (*Obje
return s.repo.getByID(ctx, objectID)
}
// CreateFromReader streams bytes directly to GCS using the storage client and
// records the depot_objects row in one shot. Unlike PrepareUpload, there is no
// signed URL or client round-trip — the caller already has the bytes. Intended
// for worker-side flows (e.g. transcoded media variants).
func (s *serviceImpl) CreateFromReader(ctx context.Context, input CreateFromReaderInput, body io.Reader) (*Object, error) {
if input.Name == "" {
return nil, errors.Join(ErrInvalidInput, errors.New("name is required"))
}
if input.ContentType == "" {
return nil, errors.Join(ErrInvalidInput, errors.New("content_type is required"))
}
objectKey := fmt.Sprintf("%s/%s/%s", input.Prefix, uuid.New().String(), input.Name)
w := s.storageClient.Bucket(s.bucketName).Object(objectKey).NewWriter(ctx)
w.ContentType = input.ContentType
if _, err := io.Copy(w, body); err != nil {
// Close to release resources, then surface the original copy error.
if cerr := w.Close(); cerr != nil {
slog.Warn("failed to close GCS writer after copy failure", "error", cerr, "object_key", objectKey)
}
slog.Error("failed to stream object to GCS", "error", err, "bucket", s.bucketName, "object_key", objectKey)
return nil, err
}
if err := w.Close(); err != nil {
slog.Error("failed to close GCS writer", "error", err, "bucket", s.bucketName, "object_key", objectKey)
return nil, err
}
obj := &Object{
Name: input.Name,
ContentType: input.ContentType,
ContentLength: w.Attrs().Size,
BucketName: s.bucketName,
ObjectKey: objectKey,
ContainsContent: true,
}
created, err := s.repo.create(ctx, obj)
if err != nil {
// Best-effort: clean up the GCS object since we can't track it in the DB.
if delErr := s.storageClient.Bucket(s.bucketName).Object(objectKey).Delete(ctx); delErr != nil {
slog.Warn("failed to clean up GCS object after db create failure", "error", delErr, "object_key", objectKey)
}
return nil, err
}
return created, nil
}
func (s *serviceImpl) GetByID(ctx context.Context, objectID string) (*Object, error) {
obj, err := s.repo.getByID(ctx, objectID)
if err != nil {