feat(v0.501): Sprint 1 -- infrastructure foundations
- Add MinIO S3-compatible storage to docker-compose (dev, staging, prod) - Create migrations 103-108 (waveform_url, user_folders, user_files, user_storage_quotas, gear_items.is_public, gear_images) - Add Go models: UserFile, UserFolder, StorageQuota, GearImage - Add WaveformURL to Track model, IsPublic + GearImages to GearItem model
This commit is contained in:
parent
03d9517f2c
commit
89cc015e54
15 changed files with 293 additions and 1 deletions
|
|
@ -166,6 +166,11 @@ services:
|
|||
- ENABLE_CLAMAV=true
|
||||
- CLAMAV_REQUIRED=true
|
||||
- CLAMAV_ADDRESS=clamav:3310
|
||||
- AWS_S3_ENDPOINT=http://minio:9000
|
||||
- AWS_S3_BUCKET=veza-files
|
||||
- AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY:?S3_ACCESS_KEY must be set}
|
||||
- AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY:?S3_SECRET_KEY must be set}
|
||||
- AWS_REGION=${AWS_REGION:-us-east-1}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
@ -233,6 +238,41 @@ services:
|
|||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: veza_minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${S3_ACCESS_KEY:?S3_ACCESS_KEY must be set}
|
||||
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:?S3_SECRET_KEY must be set}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
networks:
|
||||
- veza-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set veza http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD};
|
||||
mc mb --ignore-existing veza/veza-files;
|
||||
exit 0;
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${S3_ACCESS_KEY:?S3_ACCESS_KEY must be set}
|
||||
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:?S3_SECRET_KEY must be set}
|
||||
networks:
|
||||
- veza-network
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./apps/web
|
||||
|
|
@ -300,3 +340,4 @@ volumes:
|
|||
redis_data:
|
||||
rabbitmq_data:
|
||||
hyperswitch_postgres_data:
|
||||
minio_data:
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ services:
|
|||
- COOKIE_HTTP_ONLY=true
|
||||
- COOKIE_PATH=/
|
||||
- CORS_ALLOWED_ORIGINS=${STAGING_CORS_ORIGINS:-https://staging.veza.app,https://staging-api.veza.app}
|
||||
- AWS_S3_ENDPOINT=http://minio:9000
|
||||
- AWS_S3_BUCKET=veza-files
|
||||
- AWS_ACCESS_KEY_ID=${STAGING_S3_ACCESS_KEY:?STAGING_S3_ACCESS_KEY must be set}
|
||||
- AWS_SECRET_ACCESS_KEY=${STAGING_S3_SECRET_KEY:?STAGING_S3_SECRET_KEY must be set}
|
||||
- AWS_REGION=us-east-1
|
||||
volumes:
|
||||
- veza_logs_staging:/var/log/veza
|
||||
depends_on:
|
||||
|
|
@ -171,6 +176,37 @@ services:
|
|||
- stream-server
|
||||
- frontend
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: veza_minio_staging
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${STAGING_S3_ACCESS_KEY:?STAGING_S3_ACCESS_KEY must be set}
|
||||
MINIO_ROOT_PASSWORD: ${STAGING_S3_SECRET_KEY:?STAGING_S3_SECRET_KEY must be set}
|
||||
volumes:
|
||||
- minio_staging_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set veza http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD};
|
||||
mc mb --ignore-existing veza/veza-files;
|
||||
exit 0;
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${STAGING_S3_ACCESS_KEY:?STAGING_S3_ACCESS_KEY must be set}
|
||||
MINIO_ROOT_PASSWORD: ${STAGING_S3_SECRET_KEY:?STAGING_S3_SECRET_KEY must be set}
|
||||
|
||||
volumes:
|
||||
postgres_staging_data:
|
||||
redis_staging_data:
|
||||
|
|
@ -178,4 +214,5 @@ volumes:
|
|||
veza_logs_staging:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
minio_staging_data:
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@ services:
|
|||
- ENABLE_CLAMAV=true
|
||||
- CLAMAV_REQUIRED=false
|
||||
- CLAMAV_ADDRESS=clamav:3310
|
||||
- AWS_S3_ENDPOINT=http://minio:9000
|
||||
- AWS_S3_BUCKET=veza-files
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
- AWS_REGION=us-east-1
|
||||
ports:
|
||||
- "${PORT_BACKEND:-18080}:8080"
|
||||
depends_on:
|
||||
|
|
@ -235,6 +240,11 @@ services:
|
|||
- JWT_SECRET=${JWT_SECRET:-dev-secret-key-minimum-32-characters-long}
|
||||
- SECRET_KEY=${JWT_SECRET:-dev-secret-key-minimum-32-characters-long}
|
||||
- PORT=3001
|
||||
- AWS_S3_ENDPOINT=http://minio:9000
|
||||
- AWS_S3_BUCKET=veza-files
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
- AWS_REGION=us-east-1
|
||||
ports:
|
||||
- "${PORT_STREAM:-18082}:3001"
|
||||
depends_on:
|
||||
|
|
@ -250,11 +260,53 @@ services:
|
|||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# MinIO - S3-compatible object storage (v0.501 Cloud Storage)
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: veza_minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
ports:
|
||||
- "${PORT_MINIO:-19000}:9000"
|
||||
- "${PORT_MINIO_CONSOLE:-19001}:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- veza-net
|
||||
|
||||
# MinIO bucket initialization
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set veza http://minio:9000 $${MINIO_ROOT_USER:-minioadmin} $${MINIO_ROOT_PASSWORD:-minioadmin};
|
||||
mc mb --ignore-existing veza/veza-files;
|
||||
mc anonymous set download veza/veza-files/public;
|
||||
exit 0;
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
networks:
|
||||
- veza-net
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
rabbitmq_data:
|
||||
hyperswitch_postgres_data:
|
||||
minio_data:
|
||||
|
||||
networks:
|
||||
veza-net:
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ type GearItem struct {
|
|||
Notes string `gorm:"type:text" json:"notes" db:"notes"`
|
||||
Documents []map[string]interface{} `gorm:"type:jsonb;default:'[]'" json:"documents" db:"documents"`
|
||||
MaintenanceHistory []map[string]interface{} `gorm:"type:jsonb;default:'[]'" json:"maintenanceHistory" db:"maintenance_history"`
|
||||
IsPublic bool `gorm:"default:false" json:"is_public" db:"is_public"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" db:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" db:"deleted_at"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
GearImages []GearImage `gorm:"foreignKey:GearID;constraint:OnDelete:CASCADE" json:"gear_images,omitempty"`
|
||||
}
|
||||
|
||||
// TableName defines the table name for GORM
|
||||
|
|
|
|||
29
veza-backend-api/internal/models/gear_image.go
Normal file
29
veza-backend-api/internal/models/gear_image.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GearImage struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
GearID uuid.UUID `gorm:"type:uuid;not null" json:"gear_id"`
|
||||
ImageURL string `gorm:"size:500;not null" json:"image_url"`
|
||||
Position int `gorm:"not null;default:0" json:"position"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
GearItem GearItem `gorm:"foreignKey:GearID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
}
|
||||
|
||||
func (GearImage) TableName() string {
|
||||
return "gear_images"
|
||||
}
|
||||
|
||||
func (m *GearImage) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == uuid.Nil {
|
||||
m.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
15
veza-backend-api/internal/models/storage_quota.go
Normal file
15
veza-backend-api/internal/models/storage_quota.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package models
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type StorageQuota struct {
|
||||
UserID uuid.UUID `gorm:"type:uuid;primaryKey" json:"user_id"`
|
||||
MaxBytes int64 `gorm:"not null;default:5368709120" json:"max_bytes"`
|
||||
UsedBytes int64 `gorm:"not null;default:0" json:"used_bytes"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
}
|
||||
|
||||
func (StorageQuota) TableName() string {
|
||||
return "user_storage_quotas"
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ type Track struct {
|
|||
Bitrate int `gorm:"default:0" json:"bitrate" db:"bitrate"` // kbps
|
||||
SampleRate int `gorm:"default:0" json:"sample_rate" db:"sample_rate"` // Hz
|
||||
WaveformPath string `gorm:"size:500" json:"waveform_path" db:"waveform_path"`
|
||||
WaveformURL *string `gorm:"size:500" json:"waveform_url,omitempty" db:"waveform_url"`
|
||||
CoverArtPath string `gorm:"size:500" json:"cover_art_path" db:"cover_art_path"`
|
||||
IsPublic bool `gorm:"default:true" json:"is_public" db:"is_public"`
|
||||
Status TrackStatus `gorm:"default:'uploading'" json:"status" db:"status"`
|
||||
|
|
|
|||
34
veza-backend-api/internal/models/user_file.go
Normal file
34
veza-backend-api/internal/models/user_file.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserFile struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
||||
FolderID *uuid.UUID `gorm:"type:uuid" json:"folder_id,omitempty"`
|
||||
Filename string `gorm:"size:255;not null" json:"filename"`
|
||||
S3Key string `gorm:"size:500;not null" json:"s3_key"`
|
||||
SizeBytes int64 `gorm:"not null;default:0" json:"size_bytes"`
|
||||
MimeType string `gorm:"size:100;not null;default:'application/octet-stream'" json:"mime_type"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
Folder *UserFolder `gorm:"foreignKey:FolderID;constraint:OnDelete:SET NULL" json:"-"`
|
||||
}
|
||||
|
||||
func (UserFile) TableName() string {
|
||||
return "user_files"
|
||||
}
|
||||
|
||||
func (m *UserFile) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == uuid.Nil {
|
||||
m.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
veza-backend-api/internal/models/user_folder.go
Normal file
33
veza-backend-api/internal/models/user_folder.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserFolder struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
ParentID *uuid.UUID `gorm:"type:uuid" json:"parent_id,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
Parent *UserFolder `gorm:"foreignKey:ParentID;constraint:OnDelete:CASCADE" json:"-"`
|
||||
Children []UserFolder `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
Files []UserFile `gorm:"foreignKey:FolderID" json:"files,omitempty"`
|
||||
}
|
||||
|
||||
func (UserFolder) TableName() string {
|
||||
return "user_folders"
|
||||
}
|
||||
|
||||
func (m *UserFolder) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == uuid.Nil {
|
||||
m.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
2
veza-backend-api/migrations/103_tracks_waveform.sql
Normal file
2
veza-backend-api/migrations/103_tracks_waveform.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- Migration 103: Add waveform_url to tracks (v0.501 S1.5)
|
||||
ALTER TABLE tracks ADD COLUMN IF NOT EXISTS waveform_url VARCHAR(500);
|
||||
12
veza-backend-api/migrations/104_user_folders.sql
Normal file
12
veza-backend-api/migrations/104_user_folders.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration 104: Create user_folders table (v0.501 C1.1)
|
||||
CREATE TABLE IF NOT EXISTS user_folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
parent_id UUID REFERENCES user_folders(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_folders_user_id ON user_folders(user_id);
|
||||
CREATE INDEX idx_user_folders_parent_id ON user_folders(parent_id);
|
||||
16
veza-backend-api/migrations/105_user_files.sql
Normal file
16
veza-backend-api/migrations/105_user_files.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- Migration 105: Create user_files table (v0.501 C1.1)
|
||||
CREATE TABLE IF NOT EXISTS user_files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
s3_key VARCHAR(500) NOT NULL,
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
mime_type VARCHAR(100) NOT NULL DEFAULT 'application/octet-stream',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_files_user_id ON user_files(user_id);
|
||||
CREATE INDEX idx_user_files_folder_id ON user_files(folder_id);
|
||||
CREATE INDEX idx_user_files_mime_type ON user_files(mime_type);
|
||||
6
veza-backend-api/migrations/106_user_storage_quotas.sql
Normal file
6
veza-backend-api/migrations/106_user_storage_quotas.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- Migration 106: Create user_storage_quotas table (v0.501 C1.1)
|
||||
CREATE TABLE IF NOT EXISTS user_storage_quotas (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
max_bytes BIGINT NOT NULL DEFAULT 5368709120,
|
||||
used_bytes BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
2
veza-backend-api/migrations/107_gear_is_public.sql
Normal file
2
veza-backend-api/migrations/107_gear_is_public.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- Migration 107: Add is_public to gear_items (v0.501 G1.1)
|
||||
ALTER TABLE gear_items ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT false;
|
||||
10
veza-backend-api/migrations/108_gear_images.sql
Normal file
10
veza-backend-api/migrations/108_gear_images.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Migration 108: Create gear_images table (v0.501 G1.2)
|
||||
CREATE TABLE IF NOT EXISTS gear_images (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
gear_id UUID NOT NULL REFERENCES gear_items(id) ON DELETE CASCADE,
|
||||
image_url VARCHAR(500) NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_gear_images_gear_id ON gear_images(gear_id);
|
||||
Loading…
Reference in a new issue