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:
@@ -15,7 +15,7 @@ import (
|
||||
stripesub "github.com/stripe/stripe-go/v85/subscription"
|
||||
)
|
||||
|
||||
//go:generate mockgen -source ./service.go -destination ./mocks/service.go
|
||||
//go:generate go tool mockgen -source ./service.go -destination ./mocks/service.go
|
||||
|
||||
type Service interface {
|
||||
GetStatus(ctx context.Context, networkID string) (*Status, error)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"cloud.google.com/go/firestore"
|
||||
)
|
||||
|
||||
//go:generate mockgen -source ./membershippublisher.go -destination ./mocks/membershippublisher.go
|
||||
//go:generate go tool mockgen -source ./membershippublisher.go -destination ./mocks/membershippublisher.go
|
||||
|
||||
// MembershipPublisher publishes network membership changes to the live store
|
||||
// (Firestore) that clients subscribe to. Postgres remains the source of truth;
|
||||
|
||||
@@ -35,11 +35,13 @@ type FirestoreTranscript struct {
|
||||
}
|
||||
|
||||
type FirestoreMediaParticleProperties struct {
|
||||
ObjectId string `firestore:"object_id"`
|
||||
MimeType string `firestore:"mime_type"`
|
||||
DurationMs int `firestore:"duration_ms"`
|
||||
SizeBytes int `firestore:"size_bytes"`
|
||||
Transcript *FirestoreTranscript `firestore:"transcript,omitempty"`
|
||||
ObjectId string `firestore:"object_id"`
|
||||
MimeType string `firestore:"mime_type"`
|
||||
DurationMs int `firestore:"duration_ms"`
|
||||
SizeBytes int `firestore:"size_bytes"`
|
||||
Transcript *FirestoreTranscript `firestore:"transcript,omitempty"`
|
||||
TranscodedObjectId string `firestore:"transcoded_object_id,omitempty"`
|
||||
TranscodedMimeType string `firestore:"transcoded_mime_type,omitempty"`
|
||||
}
|
||||
|
||||
type FirestoreStreamParticle struct {
|
||||
|
||||
@@ -2,7 +2,7 @@ package particle
|
||||
|
||||
import "context"
|
||||
|
||||
//go:generate mockgen -source ./networkmembershipchecker.go -destination ./mocks/networkmembershipchecker.go
|
||||
//go:generate go tool mockgen -source ./networkmembershipchecker.go -destination ./mocks/networkmembershipchecker.go
|
||||
|
||||
type NetworkMembershipChecker interface {
|
||||
// IsMember returns true if the humanId is a member of the network.
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// directive lives here rather than next to the source.
|
||||
package aero
|
||||
|
||||
//go:generate mockgen -destination ./mock_aero.go -package aero github.com/flowy-live/llink/genproto/aero PrimaryClient
|
||||
//go:generate go tool mockgen -destination ./mock_aero.go -package aero github.com/flowy-live/llink/genproto/aero PrimaryClient
|
||||
|
||||
Reference in New Issue
Block a user