@weirdfingers/baseboards 0.9.5 → 0.9.7

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.
Files changed (237) hide show
  1. package/dist/index.js +561 -469
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -5
  4. package/templates/README.md +0 -122
  5. package/templates/api/.env.example +0 -65
  6. package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
  7. package/templates/api/Dockerfile +0 -32
  8. package/templates/api/README.md +0 -264
  9. package/templates/api/alembic/env.py +0 -114
  10. package/templates/api/alembic/script.py.mako +0 -28
  11. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
  12. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
  13. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
  14. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
  16. package/templates/api/alembic.ini +0 -36
  17. package/templates/api/config/generators.yaml +0 -237
  18. package/templates/api/config/storage_config.yaml +0 -26
  19. package/templates/api/docs/ADDING_GENERATORS.md +0 -409
  20. package/templates/api/docs/GENERATORS_API.md +0 -502
  21. package/templates/api/docs/MIGRATIONS.md +0 -472
  22. package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
  23. package/templates/api/docs/storage_providers.md +0 -337
  24. package/templates/api/pyproject.toml +0 -205
  25. package/templates/api/src/boards/__init__.py +0 -10
  26. package/templates/api/src/boards/api/app.py +0 -172
  27. package/templates/api/src/boards/api/auth.py +0 -75
  28. package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
  29. package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
  30. package/templates/api/src/boards/api/endpoints/setup.py +0 -505
  31. package/templates/api/src/boards/api/endpoints/sse.py +0 -129
  32. package/templates/api/src/boards/api/endpoints/storage.py +0 -155
  33. package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
  34. package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
  35. package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
  36. package/templates/api/src/boards/auth/__init__.py +0 -15
  37. package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
  38. package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
  39. package/templates/api/src/boards/auth/adapters/base.py +0 -73
  40. package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
  41. package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
  42. package/templates/api/src/boards/auth/adapters/none.py +0 -102
  43. package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
  44. package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
  45. package/templates/api/src/boards/auth/context.py +0 -35
  46. package/templates/api/src/boards/auth/factory.py +0 -129
  47. package/templates/api/src/boards/auth/middleware.py +0 -221
  48. package/templates/api/src/boards/auth/provisioning.py +0 -129
  49. package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
  50. package/templates/api/src/boards/cli.py +0 -354
  51. package/templates/api/src/boards/config.py +0 -131
  52. package/templates/api/src/boards/database/__init__.py +0 -7
  53. package/templates/api/src/boards/database/cli.py +0 -110
  54. package/templates/api/src/boards/database/connection.py +0 -292
  55. package/templates/api/src/boards/database/models.py +0 -19
  56. package/templates/api/src/boards/database/seed_data.py +0 -182
  57. package/templates/api/src/boards/dbmodels/__init__.py +0 -441
  58. package/templates/api/src/boards/generators/__init__.py +0 -57
  59. package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
  60. package/templates/api/src/boards/generators/artifacts.py +0 -53
  61. package/templates/api/src/boards/generators/base.py +0 -144
  62. package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
  63. package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
  64. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
  65. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
  66. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
  67. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
  68. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
  69. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
  70. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
  71. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
  72. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
  73. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
  74. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
  75. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
  76. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
  77. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
  78. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
  79. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
  80. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
  81. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
  82. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
  83. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
  84. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
  85. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
  86. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
  87. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
  88. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
  89. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
  90. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
  91. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
  92. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
  93. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
  94. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
  95. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
  96. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
  97. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
  98. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
  99. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
  100. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
  101. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
  102. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
  103. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
  104. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
  105. package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
  106. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
  107. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
  108. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
  109. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
  110. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
  111. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
  112. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
  113. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
  114. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
  115. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
  116. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
  117. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
  118. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
  119. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
  120. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
  121. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
  122. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
  123. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
  124. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
  125. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
  126. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
  127. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
  128. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
  129. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
  130. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
  131. package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
  132. package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
  133. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
  134. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
  135. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
  136. package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
  137. package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
  138. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
  139. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
  140. package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
  141. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
  142. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
  143. package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
  144. package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
  145. package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
  146. package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
  147. package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
  148. package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
  149. package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
  150. package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
  151. package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
  152. package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
  153. package/templates/api/src/boards/generators/loader.py +0 -253
  154. package/templates/api/src/boards/generators/registry.py +0 -114
  155. package/templates/api/src/boards/generators/resolution.py +0 -632
  156. package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
  157. package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
  158. package/templates/api/src/boards/graphql/__init__.py +0 -7
  159. package/templates/api/src/boards/graphql/access_control.py +0 -136
  160. package/templates/api/src/boards/graphql/mutations/root.py +0 -148
  161. package/templates/api/src/boards/graphql/queries/root.py +0 -116
  162. package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
  163. package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
  164. package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
  165. package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
  166. package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
  167. package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
  168. package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
  169. package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
  170. package/templates/api/src/boards/graphql/schema.py +0 -81
  171. package/templates/api/src/boards/graphql/types/board.py +0 -102
  172. package/templates/api/src/boards/graphql/types/generation.py +0 -166
  173. package/templates/api/src/boards/graphql/types/generator.py +0 -17
  174. package/templates/api/src/boards/graphql/types/user.py +0 -47
  175. package/templates/api/src/boards/jobs/repository.py +0 -153
  176. package/templates/api/src/boards/logging.py +0 -195
  177. package/templates/api/src/boards/middleware.py +0 -339
  178. package/templates/api/src/boards/progress/__init__.py +0 -4
  179. package/templates/api/src/boards/progress/models.py +0 -25
  180. package/templates/api/src/boards/progress/publisher.py +0 -64
  181. package/templates/api/src/boards/py.typed +0 -0
  182. package/templates/api/src/boards/redis_pool.py +0 -118
  183. package/templates/api/src/boards/storage/__init__.py +0 -52
  184. package/templates/api/src/boards/storage/base.py +0 -363
  185. package/templates/api/src/boards/storage/config.py +0 -187
  186. package/templates/api/src/boards/storage/factory.py +0 -288
  187. package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
  188. package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
  189. package/templates/api/src/boards/storage/implementations/local.py +0 -201
  190. package/templates/api/src/boards/storage/implementations/s3.py +0 -294
  191. package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
  192. package/templates/api/src/boards/tenant_isolation.py +0 -446
  193. package/templates/api/src/boards/validation.py +0 -262
  194. package/templates/api/src/boards/workers/__init__.py +0 -1
  195. package/templates/api/src/boards/workers/actors.py +0 -274
  196. package/templates/api/src/boards/workers/cli.py +0 -125
  197. package/templates/api/src/boards/workers/context.py +0 -348
  198. package/templates/api/src/boards/workers/middleware.py +0 -58
  199. package/templates/api/src/py.typed +0 -0
  200. package/templates/compose.web.yaml +0 -35
  201. package/templates/compose.yaml +0 -116
  202. package/templates/docker/env.example +0 -23
  203. package/templates/web/.env.example +0 -28
  204. package/templates/web/Dockerfile +0 -51
  205. package/templates/web/components.json +0 -22
  206. package/templates/web/imageLoader.js +0 -18
  207. package/templates/web/next-env.d.ts +0 -5
  208. package/templates/web/next.config.js +0 -36
  209. package/templates/web/package.json +0 -41
  210. package/templates/web/postcss.config.mjs +0 -7
  211. package/templates/web/public/favicon.ico +0 -0
  212. package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
  213. package/templates/web/src/app/globals.css +0 -123
  214. package/templates/web/src/app/layout.tsx +0 -31
  215. package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
  216. package/templates/web/src/app/page.tsx +0 -35
  217. package/templates/web/src/app/providers.tsx +0 -18
  218. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
  219. package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
  220. package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
  221. package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
  222. package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
  223. package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
  224. package/templates/web/src/components/header.tsx +0 -32
  225. package/templates/web/src/components/theme-provider.tsx +0 -10
  226. package/templates/web/src/components/theme-toggle.tsx +0 -75
  227. package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
  228. package/templates/web/src/components/ui/button.tsx +0 -58
  229. package/templates/web/src/components/ui/card.tsx +0 -92
  230. package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
  231. package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
  232. package/templates/web/src/components/ui/toast.tsx +0 -128
  233. package/templates/web/src/components/ui/toaster.tsx +0 -35
  234. package/templates/web/src/components/ui/use-toast.ts +0 -187
  235. package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
  236. package/templates/web/src/lib/utils.ts +0 -6
  237. package/templates/web/tsconfig.json +0 -41
@@ -1,363 +0,0 @@
1
- """Core storage interfaces and manager implementation."""
2
-
3
- import asyncio
4
- import re
5
- import uuid
6
- from abc import ABC, abstractmethod
7
- from collections.abc import AsyncIterator
8
- from dataclasses import dataclass, field
9
- from datetime import UTC, datetime, timedelta
10
- from typing import Any
11
-
12
- from ..logging import get_logger
13
-
14
- logger = get_logger(__name__)
15
-
16
-
17
- @dataclass
18
- class StorageConfig:
19
- """Configuration for storage system."""
20
-
21
- default_provider: str
22
- providers: dict[str, dict[str, Any]]
23
- routing_rules: list[dict[str, Any]]
24
- max_file_size: int = 100 * 1024 * 1024 # 100MB default
25
- allowed_content_types: set[str] = field(default_factory=set)
26
-
27
- def __post_init__(self):
28
- if not self.allowed_content_types:
29
- self.allowed_content_types = {
30
- "image/jpeg",
31
- "image/png",
32
- "image/webp",
33
- "image/gif",
34
- "video/mp4",
35
- "video/webm",
36
- "video/quicktime",
37
- "audio/mpeg",
38
- "audio/wav",
39
- "audio/ogg",
40
- "text/plain",
41
- "application/json",
42
- "text/markdown",
43
- "application/octet-stream", # For model files
44
- }
45
-
46
-
47
- @dataclass
48
- class ArtifactReference:
49
- """Reference to a stored artifact."""
50
-
51
- artifact_id: str
52
- storage_key: str
53
- storage_provider: str
54
- storage_url: str
55
- content_type: str
56
- size: int = 0
57
- created_at: datetime | None = None
58
-
59
- def __post_init__(self):
60
- if self.created_at is None:
61
- self.created_at = datetime.now(UTC)
62
-
63
-
64
- class StorageException(Exception):
65
- """Base exception for storage operations."""
66
-
67
- pass
68
-
69
-
70
- class SecurityException(StorageException):
71
- """Security-related storage exception."""
72
-
73
- pass
74
-
75
-
76
- class ValidationException(StorageException):
77
- """Content validation exception."""
78
-
79
- pass
80
-
81
-
82
- class StorageProvider(ABC):
83
- """Abstract base class for all storage providers."""
84
-
85
- @abstractmethod
86
- async def upload(
87
- self,
88
- key: str,
89
- content: bytes | AsyncIterator[bytes],
90
- content_type: str,
91
- metadata: dict[str, Any] | None = None,
92
- ) -> str:
93
- """Upload content and return storage reference.
94
-
95
- Args:
96
- key: Storage key (must be validated before calling)
97
- content: File content as bytes or async iterator
98
- content_type: MIME type (must be validated)
99
- metadata: Optional metadata dictionary
100
-
101
- Returns:
102
- storage reference
103
-
104
- Raises:
105
- StorageException: On upload failure
106
- SecurityException: On security validation failure
107
- """
108
- pass
109
-
110
- @abstractmethod
111
- async def download(self, key: str) -> bytes:
112
- """Download content by storage key."""
113
- pass
114
-
115
- @abstractmethod
116
- async def get_presigned_upload_url(
117
- self, key: str, content_type: str, expires_in: timedelta | None = None
118
- ) -> dict[str, Any]:
119
- """Generate presigned URL for direct client uploads."""
120
- pass
121
-
122
- @abstractmethod
123
- async def get_presigned_download_url(
124
- self, key: str, expires_in: timedelta | None = None
125
- ) -> str:
126
- """Generate presigned URL for secure downloads."""
127
- pass
128
-
129
- @abstractmethod
130
- async def delete(self, key: str) -> bool:
131
- """Delete file by storage key."""
132
- pass
133
-
134
- @abstractmethod
135
- async def exists(self, key: str) -> bool:
136
- """Check if file exists."""
137
- pass
138
-
139
- @abstractmethod
140
- async def get_metadata(self, key: str) -> dict[str, Any]:
141
- """Get file metadata (size, modified date, etc.)."""
142
- pass
143
-
144
-
145
- class StorageManager:
146
- """Central storage coordinator handling provider selection and routing."""
147
-
148
- def __init__(self, config: StorageConfig):
149
- self.providers: dict[str, StorageProvider] = {}
150
- self.default_provider = config.default_provider
151
- self.routing_rules = config.routing_rules
152
- self.config = config
153
-
154
- def _validate_storage_key(self, key: str) -> str:
155
- """Validate and sanitize storage key to prevent path traversal."""
156
- # Remove any path traversal attempts
157
- if ".." in key or key.startswith("/") or "\\" in key:
158
- raise SecurityException(f"Invalid storage key: {key}")
159
-
160
- # Sanitize key components
161
- key_parts = key.split("/")
162
- sanitized_parts: list[str] = []
163
-
164
- for part in key_parts:
165
- # Remove dangerous characters, keep alphanumeric, hyphens, underscores, dots
166
- sanitized = re.sub(r"[^a-zA-Z0-9._-]", "", part)
167
- if not sanitized:
168
- raise SecurityException(f"Invalid key component: {part}")
169
- sanitized_parts.append(sanitized)
170
-
171
- return "/".join(sanitized_parts)
172
-
173
- def _validate_content_type(self, content_type: str) -> None:
174
- """Validate content type against allowed types."""
175
- if content_type not in self.config.allowed_content_types:
176
- raise ValidationException(f"Content type not allowed: {content_type}")
177
-
178
- def _validate_file_size(self, content_size: int) -> None:
179
- """Validate file size against limits."""
180
- if content_size > self.config.max_file_size:
181
- raise ValidationException(
182
- f"File size {content_size} exceeds limit {self.config.max_file_size}"
183
- )
184
-
185
- def register_provider(self, name: str, provider: StorageProvider):
186
- """Register a storage provider."""
187
- self.providers[name] = provider
188
-
189
- async def store_artifact(
190
- self,
191
- artifact_id: str,
192
- content: bytes | AsyncIterator[bytes],
193
- artifact_type: str,
194
- content_type: str,
195
- tenant_id: str | None = None,
196
- board_id: str | None = None,
197
- ) -> ArtifactReference:
198
- """Store artifact with comprehensive validation and error handling."""
199
-
200
- try:
201
- # Validate content type
202
- self._validate_content_type(content_type)
203
-
204
- # Validate content size if it's bytes
205
- if isinstance(content, bytes):
206
- self._validate_file_size(len(content))
207
-
208
- # Generate and validate storage key
209
- key = self._generate_storage_key(artifact_id, artifact_type, tenant_id, board_id)
210
- validated_key = self._validate_storage_key(key)
211
-
212
- # Select provider based on routing rules
213
- provider_name = self._select_provider(artifact_type, content)
214
- if provider_name not in self.providers:
215
- raise StorageException(f"Provider not found: {provider_name}")
216
-
217
- provider = self.providers[provider_name]
218
-
219
- # Prepare metadata
220
- metadata = {
221
- "artifact_id": artifact_id,
222
- "artifact_type": artifact_type,
223
- "tenant_id": tenant_id,
224
- "board_id": board_id,
225
- "uploaded_at": datetime.now(UTC).isoformat(),
226
- "content_type": content_type,
227
- }
228
-
229
- # Store the content with retry logic
230
- storage_url = await self._upload_with_retry(
231
- provider, validated_key, content, content_type, metadata
232
- )
233
-
234
- logger.info(f"Successfully stored artifact {artifact_id} at {validated_key}")
235
-
236
- return ArtifactReference(
237
- artifact_id=artifact_id,
238
- storage_key=validated_key,
239
- storage_provider=provider_name,
240
- storage_url=storage_url,
241
- content_type=content_type,
242
- size=len(content) if isinstance(content, bytes) else 0,
243
- created_at=datetime.now(UTC),
244
- )
245
-
246
- except (SecurityException, ValidationException) as e:
247
- logger.error(f"Validation failed for artifact {artifact_id}: {e}")
248
- raise
249
- except Exception as e:
250
- logger.error(f"Failed to store artifact {artifact_id}: {e}")
251
- raise StorageException(f"Storage operation failed: {e}") from e
252
-
253
- async def _upload_with_retry(
254
- self,
255
- provider: StorageProvider,
256
- key: str,
257
- content: bytes | AsyncIterator[bytes],
258
- content_type: str,
259
- metadata: dict[str, Any],
260
- max_retries: int = 3,
261
- ) -> str:
262
- """Upload with exponential backoff retry logic."""
263
-
264
- if max_retries <= 0:
265
- max_retries = 1
266
-
267
- for attempt in range(max_retries):
268
- try:
269
- return await provider.upload(key, content, content_type, metadata)
270
- except Exception as e:
271
- if attempt == max_retries - 1:
272
- raise
273
-
274
- wait_time = 2**attempt # Exponential backoff
275
- logger.warning(
276
- f"Upload attempt {attempt + 1} failed: {e}. Retrying in {wait_time}s"
277
- )
278
- await asyncio.sleep(wait_time)
279
-
280
- # This should never be reached due to the exception handling above
281
- raise StorageException("Upload failed after all retries")
282
-
283
- def _generate_storage_key(
284
- self,
285
- artifact_id: str,
286
- artifact_type: str,
287
- tenant_id: str | None = None,
288
- board_id: str | None = None,
289
- variant: str = "original",
290
- ) -> str:
291
- """Generate hierarchical storage key with collision prevention."""
292
-
293
- # Use tenant_id or default
294
- tenant = tenant_id or "default"
295
-
296
- # Add timestamp and UUID for uniqueness
297
- timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
298
- unique_suffix = str(uuid.uuid4())[:8]
299
-
300
- if board_id:
301
- # Board-scoped artifact
302
- return f"{tenant}/{artifact_type}/{board_id}/{artifact_id}_{timestamp}_{unique_suffix}/{variant}" # noqa: E501
303
- else:
304
- # Global artifact (like LoRA models)
305
- return f"{tenant}/{artifact_type}/{artifact_id}_{timestamp}_{unique_suffix}/{variant}"
306
-
307
- def _select_provider(self, artifact_type: str, content: bytes | AsyncIterator[bytes]) -> str:
308
- """Select storage provider based on routing rules."""
309
- content_size = len(content) if isinstance(content, bytes) else 0
310
-
311
- for rule in self.routing_rules:
312
- condition = rule.get("condition", {})
313
-
314
- # Check artifact type condition
315
- if "artifact_type" in condition:
316
- if condition["artifact_type"] != artifact_type:
317
- continue
318
-
319
- # Check size condition
320
- if "size_gt" in condition:
321
- size_limit = self._parse_size(condition["size_gt"])
322
- if content_size <= size_limit:
323
- continue
324
- elif not isinstance(content, bytes):
325
- logger.warning(
326
- f"Size-based routing rule ignored for {artifact_type} - "
327
- f"content size unknown for async iterator"
328
- )
329
- continue
330
-
331
- # If all conditions match, return this provider
332
- return rule["provider"]
333
-
334
- # Return default if no rules match
335
- return self.default_provider
336
-
337
- def _parse_size(self, size_str: str) -> int:
338
- """Parse size string like '100MB' to bytes."""
339
- size_str = size_str.upper()
340
- if size_str.endswith("KB"):
341
- return int(size_str[:-2]) * 1024
342
- elif size_str.endswith("MB"):
343
- return int(size_str[:-2]) * 1024 * 1024
344
- elif size_str.endswith("GB"):
345
- return int(size_str[:-2]) * 1024 * 1024 * 1024
346
- else:
347
- return int(size_str)
348
-
349
- async def get_download_url(self, storage_key: str, provider_name: str) -> str:
350
- """Get download URL for a stored artifact."""
351
- if provider_name not in self.providers:
352
- raise StorageException(f"Provider not found: {provider_name}")
353
-
354
- provider = self.providers[provider_name]
355
- return await provider.get_presigned_download_url(storage_key)
356
-
357
- async def delete_artifact(self, storage_key: str, provider_name: str) -> bool:
358
- """Delete a stored artifact."""
359
- if provider_name not in self.providers:
360
- raise StorageException(f"Provider not found: {provider_name}")
361
-
362
- provider = self.providers[provider_name]
363
- return await provider.delete(storage_key)
@@ -1,187 +0,0 @@
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)