@weirdfingers/baseboards 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/README.md +120 -0
- package/templates/api/.env.example +62 -0
- package/templates/api/Dockerfile +32 -0
- package/templates/api/README.md +132 -0
- package/templates/api/alembic/env.py +106 -0
- package/templates/api/alembic/script.py.mako +28 -0
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
- package/templates/api/alembic.ini +36 -0
- package/templates/api/config/generators.yaml +25 -0
- package/templates/api/config/storage_config.yaml +26 -0
- package/templates/api/docs/ADDING_GENERATORS.md +409 -0
- package/templates/api/docs/GENERATORS_API.md +502 -0
- package/templates/api/docs/MIGRATIONS.md +472 -0
- package/templates/api/docs/storage_providers.md +337 -0
- package/templates/api/pyproject.toml +165 -0
- package/templates/api/src/boards/__init__.py +10 -0
- package/templates/api/src/boards/api/app.py +171 -0
- package/templates/api/src/boards/api/auth.py +75 -0
- package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
- package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
- package/templates/api/src/boards/api/endpoints/setup.py +505 -0
- package/templates/api/src/boards/api/endpoints/sse.py +129 -0
- package/templates/api/src/boards/api/endpoints/storage.py +74 -0
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
- package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
- package/templates/api/src/boards/auth/__init__.py +15 -0
- package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
- package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
- package/templates/api/src/boards/auth/adapters/base.py +73 -0
- package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
- package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
- package/templates/api/src/boards/auth/adapters/none.py +102 -0
- package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
- package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
- package/templates/api/src/boards/auth/context.py +35 -0
- package/templates/api/src/boards/auth/factory.py +115 -0
- package/templates/api/src/boards/auth/middleware.py +221 -0
- package/templates/api/src/boards/auth/provisioning.py +129 -0
- package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
- package/templates/api/src/boards/cli.py +354 -0
- package/templates/api/src/boards/config.py +116 -0
- package/templates/api/src/boards/database/__init__.py +7 -0
- package/templates/api/src/boards/database/cli.py +110 -0
- package/templates/api/src/boards/database/connection.py +252 -0
- package/templates/api/src/boards/database/models.py +19 -0
- package/templates/api/src/boards/database/seed_data.py +182 -0
- package/templates/api/src/boards/dbmodels/__init__.py +455 -0
- package/templates/api/src/boards/generators/__init__.py +57 -0
- package/templates/api/src/boards/generators/artifacts.py +53 -0
- package/templates/api/src/boards/generators/base.py +140 -0
- package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
- package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
- package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
- package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
- package/templates/api/src/boards/generators/loader.py +253 -0
- package/templates/api/src/boards/generators/registry.py +114 -0
- package/templates/api/src/boards/generators/resolution.py +515 -0
- package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
- package/templates/api/src/boards/graphql/__init__.py +7 -0
- package/templates/api/src/boards/graphql/access_control.py +136 -0
- package/templates/api/src/boards/graphql/mutations/root.py +136 -0
- package/templates/api/src/boards/graphql/queries/root.py +116 -0
- package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
- package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
- package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
- package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
- package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
- package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
- package/templates/api/src/boards/graphql/schema.py +81 -0
- package/templates/api/src/boards/graphql/types/board.py +102 -0
- package/templates/api/src/boards/graphql/types/generation.py +130 -0
- package/templates/api/src/boards/graphql/types/generator.py +17 -0
- package/templates/api/src/boards/graphql/types/user.py +47 -0
- package/templates/api/src/boards/jobs/repository.py +104 -0
- package/templates/api/src/boards/logging.py +195 -0
- package/templates/api/src/boards/middleware.py +339 -0
- package/templates/api/src/boards/progress/__init__.py +4 -0
- package/templates/api/src/boards/progress/models.py +25 -0
- package/templates/api/src/boards/progress/publisher.py +64 -0
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +118 -0
- package/templates/api/src/boards/storage/__init__.py +52 -0
- package/templates/api/src/boards/storage/base.py +363 -0
- package/templates/api/src/boards/storage/config.py +187 -0
- package/templates/api/src/boards/storage/factory.py +278 -0
- package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
- package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
- package/templates/api/src/boards/storage/implementations/local.py +201 -0
- package/templates/api/src/boards/storage/implementations/s3.py +294 -0
- package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
- package/templates/api/src/boards/tenant_isolation.py +446 -0
- package/templates/api/src/boards/validation.py +262 -0
- package/templates/api/src/boards/workers/__init__.py +1 -0
- package/templates/api/src/boards/workers/actors.py +201 -0
- package/templates/api/src/boards/workers/cli.py +125 -0
- package/templates/api/src/boards/workers/context.py +188 -0
- package/templates/api/src/boards/workers/middleware.py +58 -0
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.dev.yaml +39 -0
- package/templates/compose.yaml +109 -0
- package/templates/docker/env.example +23 -0
- package/templates/web/.env.example +28 -0
- package/templates/web/Dockerfile +51 -0
- package/templates/web/components.json +22 -0
- package/templates/web/imageLoader.js +18 -0
- package/templates/web/next-env.d.ts +5 -0
- package/templates/web/next.config.js +36 -0
- package/templates/web/package.json +37 -0
- package/templates/web/postcss.config.mjs +7 -0
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
- package/templates/web/src/app/globals.css +120 -0
- package/templates/web/src/app/layout.tsx +21 -0
- package/templates/web/src/app/page.tsx +35 -0
- package/templates/web/src/app/providers.tsx +18 -0
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
- package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
- package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
- package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
- package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
- package/templates/web/src/components/header.tsx +30 -0
- package/templates/web/src/components/ui/button.tsx +58 -0
- package/templates/web/src/components/ui/card.tsx +92 -0
- package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
- package/templates/web/src/lib/utils.ts +6 -0
- package/templates/web/tsconfig.json +47 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Storage configuration system."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .base import StorageConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProviderConfig:
|
|
15
|
+
"""Configuration for a specific storage provider."""
|
|
16
|
+
|
|
17
|
+
type: str
|
|
18
|
+
config: dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_storage_config(
|
|
22
|
+
config_path: Path | None = None, env_prefix: str = "BOARDS_STORAGE_"
|
|
23
|
+
) -> StorageConfig:
|
|
24
|
+
"""Load storage configuration from file and environment variables.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config_path: Path to YAML configuration file
|
|
28
|
+
env_prefix: Prefix for environment variable overrides
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
StorageConfig instance
|
|
32
|
+
"""
|
|
33
|
+
# Default configuration
|
|
34
|
+
config_data = {
|
|
35
|
+
"default_provider": "local",
|
|
36
|
+
"providers": {
|
|
37
|
+
"local": {
|
|
38
|
+
"type": "local",
|
|
39
|
+
"config": {
|
|
40
|
+
"base_path": "/tmp/boards/storage",
|
|
41
|
+
"public_url_base": "http://localhost:8088/api/storage",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"routing_rules": [{"provider": "local"}], # Default rule
|
|
46
|
+
"max_file_size": 100 * 1024 * 1024, # 100MB
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Load from YAML file if provided
|
|
50
|
+
if config_path and config_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
with open(config_path) as f:
|
|
53
|
+
file_config = yaml.safe_load(f)
|
|
54
|
+
if file_config.get("storage"):
|
|
55
|
+
config_data.update(file_config["storage"])
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise ValueError(f"Failed to load storage config from {config_path}: {e}") from e
|
|
58
|
+
|
|
59
|
+
# Override with environment variables
|
|
60
|
+
config_data = _apply_env_overrides(config_data, env_prefix)
|
|
61
|
+
|
|
62
|
+
return StorageConfig(
|
|
63
|
+
default_provider=config_data["default_provider"],
|
|
64
|
+
providers=config_data["providers"],
|
|
65
|
+
routing_rules=config_data["routing_rules"],
|
|
66
|
+
max_file_size=config_data.get("max_file_size", 100 * 1024 * 1024),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _apply_env_overrides(config_data: dict[str, Any], env_prefix: str) -> dict[str, Any]:
|
|
71
|
+
"""Apply environment variable overrides to configuration."""
|
|
72
|
+
|
|
73
|
+
# Override default provider
|
|
74
|
+
default_provider = os.getenv(f"{env_prefix}DEFAULT_PROVIDER")
|
|
75
|
+
if default_provider:
|
|
76
|
+
config_data["default_provider"] = default_provider
|
|
77
|
+
|
|
78
|
+
# Override max file size
|
|
79
|
+
max_file_size = os.getenv(f"{env_prefix}MAX_FILE_SIZE")
|
|
80
|
+
if max_file_size:
|
|
81
|
+
config_data["max_file_size"] = int(max_file_size)
|
|
82
|
+
|
|
83
|
+
# Provider-specific overrides
|
|
84
|
+
_apply_provider_env_overrides(config_data, env_prefix)
|
|
85
|
+
|
|
86
|
+
return config_data
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _apply_provider_env_overrides(config_data: dict[str, Any], env_prefix: str):
|
|
90
|
+
"""Apply environment variable overrides for provider configurations."""
|
|
91
|
+
|
|
92
|
+
# Supabase configuration
|
|
93
|
+
supabase_url = os.getenv("SUPABASE_URL")
|
|
94
|
+
supabase_key = os.getenv("SUPABASE_ANON_KEY")
|
|
95
|
+
supabase_bucket = os.getenv(f"{env_prefix}SUPABASE_BUCKET")
|
|
96
|
+
|
|
97
|
+
if supabase_url and supabase_key:
|
|
98
|
+
config_data["providers"]["supabase"] = {
|
|
99
|
+
"type": "supabase",
|
|
100
|
+
"config": {
|
|
101
|
+
"url": supabase_url,
|
|
102
|
+
"key": supabase_key,
|
|
103
|
+
"bucket": supabase_bucket or "boards-artifacts",
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# S3 configuration
|
|
108
|
+
s3_bucket = os.getenv(f"{env_prefix}S3_BUCKET")
|
|
109
|
+
s3_region = os.getenv(f"{env_prefix}S3_REGION")
|
|
110
|
+
aws_access_key = os.getenv("AWS_ACCESS_KEY_ID")
|
|
111
|
+
aws_secret_key = os.getenv("AWS_SECRET_ACCESS_KEY")
|
|
112
|
+
|
|
113
|
+
if s3_bucket and aws_access_key and aws_secret_key:
|
|
114
|
+
config_data["providers"]["s3"] = {
|
|
115
|
+
"type": "s3",
|
|
116
|
+
"config": {
|
|
117
|
+
"bucket": s3_bucket,
|
|
118
|
+
"region": s3_region or "us-west-2",
|
|
119
|
+
"access_key_id": aws_access_key,
|
|
120
|
+
"secret_access_key": aws_secret_key,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Local storage overrides
|
|
125
|
+
local_base_path = os.getenv(f"{env_prefix}LOCAL_BASE_PATH")
|
|
126
|
+
local_public_url = os.getenv(f"{env_prefix}LOCAL_PUBLIC_URL_BASE")
|
|
127
|
+
|
|
128
|
+
if local_base_path or local_public_url:
|
|
129
|
+
local_config = config_data["providers"].get("local", {}).get("config", {})
|
|
130
|
+
if local_base_path:
|
|
131
|
+
local_config["base_path"] = local_base_path
|
|
132
|
+
if local_public_url:
|
|
133
|
+
local_config["public_url_base"] = local_public_url
|
|
134
|
+
|
|
135
|
+
config_data["providers"]["local"] = {"type": "local", "config": local_config}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def create_example_config() -> str:
|
|
139
|
+
"""Create an example storage configuration YAML."""
|
|
140
|
+
|
|
141
|
+
config = {
|
|
142
|
+
"storage": {
|
|
143
|
+
"default_provider": "supabase",
|
|
144
|
+
"providers": {
|
|
145
|
+
"local": {
|
|
146
|
+
"type": "local",
|
|
147
|
+
"config": {
|
|
148
|
+
"base_path": "/var/boards/storage",
|
|
149
|
+
"public_url_base": "http://localhost:8088/api/storage",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
"supabase": {
|
|
153
|
+
"type": "supabase",
|
|
154
|
+
"config": {
|
|
155
|
+
"url": "${SUPABASE_URL}",
|
|
156
|
+
"key": "${SUPABASE_ANON_KEY}",
|
|
157
|
+
"bucket": "boards-artifacts",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"s3": {
|
|
161
|
+
"type": "s3",
|
|
162
|
+
"config": {
|
|
163
|
+
"bucket": "boards-prod-artifacts",
|
|
164
|
+
"region": "us-west-2",
|
|
165
|
+
"access_key_id": "${AWS_ACCESS_KEY_ID}",
|
|
166
|
+
"secret_access_key": "${AWS_SECRET_ACCESS_KEY}",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
"routing_rules": [
|
|
171
|
+
{
|
|
172
|
+
"condition": {"artifact_type": "video", "size_gt": "100MB"},
|
|
173
|
+
"provider": "s3",
|
|
174
|
+
},
|
|
175
|
+
{"condition": {"artifact_type": "model"}, "provider": "supabase"},
|
|
176
|
+
{"provider": "supabase"},
|
|
177
|
+
],
|
|
178
|
+
"max_file_size": 1073741824, # 1GB
|
|
179
|
+
"cleanup": {
|
|
180
|
+
"temp_file_ttl_hours": 24,
|
|
181
|
+
"cleanup_interval_hours": 1,
|
|
182
|
+
"max_cleanup_batch_size": 1000,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return yaml.dump(config, default_flow_style=False, indent=2)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Factory for creating storage providers and managers."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..logging import get_logger
|
|
7
|
+
from .base import StorageManager, StorageProvider
|
|
8
|
+
from .config import StorageConfig, load_storage_config
|
|
9
|
+
from .implementations.local import LocalStorageProvider
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
# Singleton storage configuration
|
|
14
|
+
# Loaded once at module import time to avoid re-parsing YAML on every request
|
|
15
|
+
_storage_config: StorageConfig | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_storage_config() -> StorageConfig:
|
|
19
|
+
"""Get the singleton storage configuration.
|
|
20
|
+
|
|
21
|
+
Loads the configuration from settings.storage_config_path on first access.
|
|
22
|
+
Subsequent calls return the cached configuration.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
StorageConfig instance
|
|
26
|
+
"""
|
|
27
|
+
global _storage_config
|
|
28
|
+
|
|
29
|
+
if _storage_config is None:
|
|
30
|
+
from ..config import settings
|
|
31
|
+
|
|
32
|
+
config_path = Path(settings.storage_config_path) if settings.storage_config_path else None
|
|
33
|
+
_storage_config = load_storage_config(config_path)
|
|
34
|
+
logger.info(
|
|
35
|
+
f"Loaded storage configuration: default_provider={_storage_config.default_provider}, "
|
|
36
|
+
f"providers={list(_storage_config.providers.keys())}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return _storage_config
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Optional imports for cloud providers
|
|
43
|
+
try:
|
|
44
|
+
from .implementations.supabase import SupabaseStorageProvider
|
|
45
|
+
|
|
46
|
+
_supabase_available = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
SupabaseStorageProvider = None
|
|
49
|
+
_supabase_available = False
|
|
50
|
+
logger.warning("Supabase storage not available - install supabase-py to enable")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from .implementations.s3 import S3StorageProvider
|
|
54
|
+
|
|
55
|
+
_s3_available = True
|
|
56
|
+
except ImportError:
|
|
57
|
+
S3StorageProvider = None
|
|
58
|
+
_s3_available = False
|
|
59
|
+
logger.warning("S3 storage not available - install boto3 and aioboto3 to enable")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
from .implementations.gcs import GCSStorageProvider
|
|
63
|
+
|
|
64
|
+
_gcs_available = True
|
|
65
|
+
except ImportError:
|
|
66
|
+
GCSStorageProvider = None
|
|
67
|
+
_gcs_available = False
|
|
68
|
+
logger.warning("GCS storage not available - install google-cloud-storage to enable")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_storage_provider(provider_type: str, config: dict[str, Any]) -> StorageProvider:
|
|
72
|
+
"""Create a storage provider instance from configuration.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
provider_type: Type of provider ('local', 'supabase', 's3')
|
|
76
|
+
config: Provider configuration dictionary
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
StorageProvider instance
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If provider type is unknown or configuration is invalid
|
|
83
|
+
ImportError: If required dependencies are not available
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if provider_type == "local":
|
|
87
|
+
return _create_local_provider(config)
|
|
88
|
+
elif provider_type == "supabase":
|
|
89
|
+
if not _supabase_available:
|
|
90
|
+
raise ImportError(
|
|
91
|
+
"Supabase storage requires supabase-py. Install with: pip install supabase"
|
|
92
|
+
)
|
|
93
|
+
return _create_supabase_provider(config)
|
|
94
|
+
elif provider_type == "s3":
|
|
95
|
+
if not _s3_available:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"S3 storage requires boto3 and aioboto3. Install with: pip install boto3 aioboto3"
|
|
98
|
+
)
|
|
99
|
+
return _create_s3_provider(config)
|
|
100
|
+
elif provider_type == "gcs":
|
|
101
|
+
if not _gcs_available:
|
|
102
|
+
raise ImportError(
|
|
103
|
+
"GCS storage package required. Install with: pip install google-cloud-storage"
|
|
104
|
+
)
|
|
105
|
+
return _create_gcs_provider(config)
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(f"Unknown storage provider type: {provider_type}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _create_local_provider(config: dict[str, Any]) -> LocalStorageProvider:
|
|
111
|
+
"""Create local storage provider."""
|
|
112
|
+
base_path = config.get("base_path", "/tmp/boards/storage")
|
|
113
|
+
public_url_base = config.get("public_url_base")
|
|
114
|
+
|
|
115
|
+
return LocalStorageProvider(base_path=Path(base_path), public_url_base=public_url_base)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _create_supabase_provider(config: dict[str, Any]) -> StorageProvider:
|
|
119
|
+
"""Create Supabase storage provider."""
|
|
120
|
+
if SupabaseStorageProvider is None:
|
|
121
|
+
raise ImportError("Supabase storage not available")
|
|
122
|
+
|
|
123
|
+
url = config.get("url")
|
|
124
|
+
key = config.get("key")
|
|
125
|
+
bucket = config.get("bucket", "boards-artifacts")
|
|
126
|
+
|
|
127
|
+
if not url:
|
|
128
|
+
raise ValueError("Supabase storage requires 'url' in configuration")
|
|
129
|
+
if not key:
|
|
130
|
+
raise ValueError("Supabase storage requires 'key' in configuration")
|
|
131
|
+
|
|
132
|
+
return SupabaseStorageProvider(url=url, key=key, bucket=bucket)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _create_s3_provider(config: dict[str, Any]) -> StorageProvider:
|
|
136
|
+
"""Create S3 storage provider."""
|
|
137
|
+
if S3StorageProvider is None:
|
|
138
|
+
raise ImportError("S3 storage not available")
|
|
139
|
+
|
|
140
|
+
bucket = config.get("bucket")
|
|
141
|
+
if not bucket:
|
|
142
|
+
raise ValueError("S3 storage requires 'bucket' in configuration")
|
|
143
|
+
|
|
144
|
+
region = config.get("region", "us-east-1")
|
|
145
|
+
aws_access_key_id = config.get("aws_access_key_id")
|
|
146
|
+
aws_secret_access_key = config.get("aws_secret_access_key")
|
|
147
|
+
aws_session_token = config.get("aws_session_token")
|
|
148
|
+
endpoint_url = config.get("endpoint_url")
|
|
149
|
+
cloudfront_domain = config.get("cloudfront_domain")
|
|
150
|
+
upload_config = config.get("upload_config", {})
|
|
151
|
+
|
|
152
|
+
return S3StorageProvider(
|
|
153
|
+
bucket=bucket,
|
|
154
|
+
region=region,
|
|
155
|
+
aws_access_key_id=aws_access_key_id,
|
|
156
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
157
|
+
aws_session_token=aws_session_token,
|
|
158
|
+
endpoint_url=endpoint_url,
|
|
159
|
+
cloudfront_domain=cloudfront_domain,
|
|
160
|
+
upload_config=upload_config,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _create_gcs_provider(config: dict[str, Any]) -> StorageProvider:
|
|
165
|
+
"""Create GCS storage provider."""
|
|
166
|
+
if GCSStorageProvider is None:
|
|
167
|
+
raise ImportError("GCS storage not available")
|
|
168
|
+
|
|
169
|
+
bucket = config.get("bucket")
|
|
170
|
+
if not bucket:
|
|
171
|
+
raise ValueError("GCS storage requires 'bucket' in configuration")
|
|
172
|
+
|
|
173
|
+
project_id = config.get("project_id")
|
|
174
|
+
credentials_path = config.get("credentials_path")
|
|
175
|
+
credentials_json = config.get("credentials_json")
|
|
176
|
+
cdn_domain = config.get("cdn_domain")
|
|
177
|
+
upload_config = config.get("upload_config", {})
|
|
178
|
+
|
|
179
|
+
return GCSStorageProvider(
|
|
180
|
+
bucket=bucket,
|
|
181
|
+
project_id=project_id,
|
|
182
|
+
credentials_path=credentials_path,
|
|
183
|
+
credentials_json=credentials_json,
|
|
184
|
+
cdn_domain=cdn_domain,
|
|
185
|
+
upload_config=upload_config,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _build_storage_manager_from_config(storage_config: StorageConfig) -> StorageManager:
|
|
190
|
+
"""Build a storage manager from a StorageConfig, registering all providers.
|
|
191
|
+
|
|
192
|
+
This is an internal helper that can be used for testing.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
storage_config: Storage configuration
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
StorageManager instance with registered providers
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
RuntimeError: If no storage providers were successfully registered
|
|
202
|
+
"""
|
|
203
|
+
# Create storage manager
|
|
204
|
+
manager = StorageManager(storage_config)
|
|
205
|
+
|
|
206
|
+
# Register providers
|
|
207
|
+
for provider_name, provider_config in storage_config.providers.items():
|
|
208
|
+
try:
|
|
209
|
+
provider_type = provider_config.get("type", provider_name)
|
|
210
|
+
provider_instance = create_storage_provider(
|
|
211
|
+
provider_type, provider_config.get("config", {})
|
|
212
|
+
)
|
|
213
|
+
manager.register_provider(provider_name, provider_instance)
|
|
214
|
+
|
|
215
|
+
logger.info(f"Registered storage provider: {provider_name} ({provider_type})")
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Failed to register provider {provider_name}: {e}")
|
|
219
|
+
# Continue with other providers rather than failing completely
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Validate default provider is available
|
|
223
|
+
if storage_config.default_provider not in manager.providers:
|
|
224
|
+
available = list(manager.providers.keys())
|
|
225
|
+
if not available:
|
|
226
|
+
raise RuntimeError("No storage providers were successfully registered")
|
|
227
|
+
|
|
228
|
+
logger.warning(
|
|
229
|
+
f"Default provider '{storage_config.default_provider}' not available. "
|
|
230
|
+
f"Using '{available[0]}' instead."
|
|
231
|
+
)
|
|
232
|
+
manager.default_provider = available[0]
|
|
233
|
+
|
|
234
|
+
return manager
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def create_storage_manager() -> StorageManager:
|
|
238
|
+
"""Create a configured storage manager using global singleton config.
|
|
239
|
+
|
|
240
|
+
The storage configuration is loaded once from settings.storage_config_path
|
|
241
|
+
and cached for the lifetime of the process.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
StorageManager instance with registered providers
|
|
245
|
+
"""
|
|
246
|
+
storage_config = get_storage_config()
|
|
247
|
+
return _build_storage_manager_from_config(storage_config)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def create_development_storage() -> StorageManager:
|
|
251
|
+
"""Create a simple storage manager for development use.
|
|
252
|
+
|
|
253
|
+
Uses local filesystem storage with sensible defaults.
|
|
254
|
+
This is primarily used for testing and creates a standalone manager
|
|
255
|
+
rather than using global settings.
|
|
256
|
+
"""
|
|
257
|
+
config = StorageConfig(
|
|
258
|
+
default_provider="local",
|
|
259
|
+
providers={
|
|
260
|
+
"local": {
|
|
261
|
+
"type": "local",
|
|
262
|
+
"config": {
|
|
263
|
+
"base_path": "/tmp/boards/storage",
|
|
264
|
+
"public_url_base": "http://localhost:8088/api/storage",
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
routing_rules=[{"provider": "local"}],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Create storage manager directly without using global settings
|
|
272
|
+
manager = StorageManager(config)
|
|
273
|
+
|
|
274
|
+
# Register the local provider
|
|
275
|
+
local_provider = create_storage_provider("local", config.providers["local"]["config"])
|
|
276
|
+
manager.register_provider("local", local_provider)
|
|
277
|
+
|
|
278
|
+
return manager
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Storage provider implementations."""
|
|
2
|
+
|
|
3
|
+
from .local import LocalStorageProvider
|
|
4
|
+
|
|
5
|
+
# Optional cloud providers - imported conditionally to avoid import errors
|
|
6
|
+
__all__ = ["LocalStorageProvider"]
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from .supabase import SupabaseStorageProvider
|
|
10
|
+
|
|
11
|
+
__all__.append("SupabaseStorageProvider")
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from .s3 import S3StorageProvider
|
|
17
|
+
|
|
18
|
+
__all__.append("S3StorageProvider")
|
|
19
|
+
except ImportError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .gcs import GCSStorageProvider
|
|
24
|
+
|
|
25
|
+
__all__.append("GCSStorageProvider")
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass
|