@weirdfingers/baseboards 0.9.6 → 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 +560 -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,201 +0,0 @@
1
- """Local filesystem storage provider for development and self-hosted deployments."""
2
-
3
- import json
4
- from collections.abc import AsyncIterable
5
- from datetime import timedelta
6
- from pathlib import Path
7
- from typing import Any
8
- from urllib.parse import quote
9
-
10
- import aiofiles
11
-
12
- from ...logging import get_logger
13
- from ..base import SecurityException, StorageException, StorageProvider
14
-
15
- logger = get_logger(__name__)
16
-
17
-
18
- class LocalStorageProvider(StorageProvider):
19
- """Local filesystem storage for development and self-hosted with security."""
20
-
21
- def __init__(self, base_path: Path, public_url_base: str | None = None):
22
- self.base_path = Path(base_path).resolve() # Resolve to absolute path
23
- self.public_url_base = public_url_base
24
- self.base_path.mkdir(parents=True, exist_ok=True)
25
-
26
- def _get_safe_file_path(self, key: str) -> Path:
27
- """Get file path with security validation."""
28
- # Ensure the resolved path is within base_path
29
- file_path = (self.base_path / key).resolve()
30
-
31
- # Check that resolved path is within base directory
32
- try:
33
- file_path.relative_to(self.base_path)
34
- except ValueError as e:
35
- raise SecurityException(f"Path traversal detected: {key}") from e
36
-
37
- return file_path
38
-
39
- async def upload(
40
- self,
41
- key: str,
42
- content: bytes | bytearray | memoryview | AsyncIterable[bytes],
43
- content_type: str,
44
- metadata: dict[str, Any] | None = None,
45
- ) -> str:
46
- logger.info("Uploading file", key=key, content_type=content_type, metadata=metadata)
47
- try:
48
- file_path = self._get_safe_file_path(key)
49
- file_path.parent.mkdir(parents=True, exist_ok=True)
50
-
51
- # Handle both bytes-like and async iterable content
52
- if isinstance(content, bytes | bytearray | memoryview):
53
- # aiofiles accepts bytes-like objects directly
54
- async with aiofiles.open(file_path, "wb") as f:
55
- await f.write(content)
56
- else: # isinstance(content, AsyncIterable):
57
- async with aiofiles.open(file_path, "wb") as f:
58
- async for chunk in content:
59
- # Just write the chunk directly - aiofiles accepts bytes-like objects
60
- # It will raise an error if chunk is not bytes-like
61
- await f.write(chunk)
62
-
63
- # Store metadata atomically
64
- if metadata:
65
- try:
66
- metadata_path = file_path.with_suffix(file_path.suffix + ".meta")
67
- metadata_json = json.dumps(metadata, indent=2)
68
-
69
- async with aiofiles.open(metadata_path, "w") as f:
70
- await f.write(metadata_json)
71
- except Exception as e:
72
- logger.warning(f"Failed to write metadata for {key}: {e}")
73
- # Continue - metadata failure shouldn't fail the upload
74
-
75
- logger.debug(f"Successfully uploaded {key} to local storage")
76
- return self._get_public_url(key)
77
-
78
- except OSError as e:
79
- logger.error(f"File system error uploading {key}: {e}")
80
- raise StorageException(f"Failed to write file: {e}") from e
81
- except Exception as e:
82
- logger.error(f"Unexpected error uploading {key}: {e}")
83
- raise StorageException(f"Upload failed: {e}") from e
84
-
85
- def _get_public_url(self, key: str) -> str:
86
- """Generate public URL for the stored file."""
87
- if self.public_url_base:
88
- # URL-encode the key for safety
89
- encoded_key = quote(key, safe="/")
90
- return f"{self.public_url_base.rstrip('/')}/{encoded_key}"
91
- else:
92
- return f"file://{self.base_path / key}"
93
-
94
- async def download(self, key: str) -> bytes:
95
- """Download file content from local storage."""
96
- try:
97
- file_path = self._get_safe_file_path(key)
98
-
99
- if not file_path.exists():
100
- raise StorageException(f"File not found: {key}")
101
-
102
- async with aiofiles.open(file_path, "rb") as f:
103
- return await f.read()
104
-
105
- except OSError as e:
106
- logger.error(f"File system error downloading {key}: {e}")
107
- raise StorageException(f"Failed to read file: {e}") from e
108
- except Exception as e:
109
- logger.error(f"Unexpected error downloading {key}: {e}")
110
- raise StorageException(f"Download failed: {e}") from e
111
-
112
- async def get_presigned_upload_url(
113
- self, key: str, content_type: str, expires_in: timedelta | None = None
114
- ) -> dict[str, Any]:
115
- """Local storage doesn't support presigned URLs - return direct upload info."""
116
- # For local storage, we can't really do presigned URLs
117
- # This would be handled by the web server (e.g., FastAPI endpoint)
118
- return {
119
- "url": f"/api/storage/upload/{quote(key, safe='/')}",
120
- "fields": {"content-type": content_type},
121
- "method": "PUT",
122
- "expires_at": None, # Handled by server session
123
- }
124
-
125
- async def get_presigned_download_url(
126
- self, key: str, expires_in: timedelta | None = None
127
- ) -> str:
128
- """Return the public URL for local storage."""
129
- return self._get_public_url(key)
130
-
131
- async def delete(self, key: str) -> bool:
132
- """Delete file by storage key."""
133
- try:
134
- file_path = self._get_safe_file_path(key)
135
-
136
- if not file_path.exists():
137
- return False
138
-
139
- # Delete the main file
140
- file_path.unlink()
141
-
142
- # Delete metadata file if it exists
143
- metadata_path = file_path.with_suffix(file_path.suffix + ".meta")
144
- if metadata_path.exists():
145
- metadata_path.unlink()
146
-
147
- logger.debug(f"Successfully deleted {key} from local storage")
148
- return True
149
-
150
- except OSError as e:
151
- logger.error(f"File system error deleting {key}: {e}")
152
- raise StorageException(f"Failed to delete file: {e}") from e
153
- except Exception as e:
154
- logger.error(f"Unexpected error deleting {key}: {e}")
155
- raise StorageException(f"Delete failed: {e}") from e
156
-
157
- async def exists(self, key: str) -> bool:
158
- """Check if file exists."""
159
- try:
160
- file_path = self._get_safe_file_path(key)
161
- return file_path.exists()
162
- except SecurityException:
163
- return False
164
- except Exception as e:
165
- logger.warning(f"Error checking existence of {key}: {e}")
166
- return False
167
-
168
- async def get_metadata(self, key: str) -> dict[str, Any]:
169
- """Get file metadata (size, modified date, etc.)."""
170
- try:
171
- file_path = self._get_safe_file_path(key)
172
-
173
- if not file_path.exists():
174
- raise StorageException(f"File not found: {key}")
175
-
176
- stat = file_path.stat()
177
-
178
- # Try to load stored metadata
179
- stored_metadata = {}
180
- metadata_path = file_path.with_suffix(file_path.suffix + ".meta")
181
- if metadata_path.exists():
182
- try:
183
- async with aiofiles.open(metadata_path) as f:
184
- metadata_content = await f.read()
185
- stored_metadata = json.loads(metadata_content)
186
- except Exception as e:
187
- logger.warning(f"Failed to load metadata for {key}: {e}")
188
-
189
- return {
190
- "size": stat.st_size,
191
- "modified_time": stat.st_mtime,
192
- "created_time": stat.st_ctime,
193
- **stored_metadata,
194
- }
195
-
196
- except OSError as e:
197
- logger.error(f"File system error getting metadata for {key}: {e}")
198
- raise StorageException(f"Failed to get metadata: {e}") from e
199
- except Exception as e:
200
- logger.error(f"Unexpected error getting metadata for {key}: {e}")
201
- raise StorageException(f"Get metadata failed: {e}") from e
@@ -1,294 +0,0 @@
1
- """AWS S3 storage provider with IAM auth and CloudFront CDN support."""
2
-
3
- from collections.abc import AsyncIterator
4
- from datetime import UTC, datetime, timedelta
5
- from typing import TYPE_CHECKING, Any
6
-
7
- if TYPE_CHECKING:
8
- import boto3
9
-
10
- try:
11
- import aioboto3
12
- import boto3
13
- from botocore.config import Config
14
- from botocore.exceptions import ClientError, NoCredentialsError
15
-
16
- _s3_available = True
17
- except ImportError:
18
- boto3 = None
19
- ClientError = None
20
- NoCredentialsError = None
21
- Config = None
22
- aioboto3 = None
23
- _s3_available = False
24
-
25
- from ...logging import get_logger
26
- from ..base import StorageException, StorageProvider
27
-
28
- logger = get_logger(__name__)
29
-
30
-
31
- class S3StorageProvider(StorageProvider):
32
- """AWS S3 storage with IAM auth, CloudFront CDN, and proper async patterns."""
33
-
34
- def __init__(
35
- self,
36
- bucket: str,
37
- region: str = "us-east-1",
38
- aws_access_key_id: str | None = None,
39
- aws_secret_access_key: str | None = None,
40
- aws_session_token: str | None = None,
41
- endpoint_url: str | None = None,
42
- cloudfront_domain: str | None = None,
43
- upload_config: dict[str, Any] | None = None,
44
- ):
45
- if not _s3_available:
46
- raise ImportError("boto3 and aioboto3 are required for S3StorageProvider")
47
-
48
- self.bucket = bucket
49
- self.region = region
50
- self.aws_access_key_id = aws_access_key_id
51
- self.aws_secret_access_key = aws_secret_access_key
52
- self.aws_session_token = aws_session_token
53
- self.endpoint_url = endpoint_url
54
- self.cloudfront_domain = cloudfront_domain
55
-
56
- # Default upload configuration
57
- self.upload_config = {
58
- "ServerSideEncryption": "AES256",
59
- "StorageClass": "STANDARD",
60
- **(upload_config or {}),
61
- }
62
-
63
- # Configure boto3 with optimized settings
64
- self.config = Config( # type: ignore[reportUnknownMemberType]
65
- region_name=self.region,
66
- retries={"max_attempts": 3, "mode": "adaptive"},
67
- max_pool_connections=50,
68
- )
69
-
70
- self._session: Any | None = None
71
-
72
- def _get_session(self) -> Any:
73
- """Get or create the aioboto3 session."""
74
- if self._session is None:
75
- self._session = aioboto3.Session( # type: ignore[reportUnknownMemberType]
76
- aws_access_key_id=self.aws_access_key_id,
77
- aws_secret_access_key=self.aws_secret_access_key,
78
- aws_session_token=self.aws_session_token,
79
- region_name=self.region,
80
- )
81
- return self._session
82
-
83
- async def upload(
84
- self,
85
- key: str,
86
- content: bytes | AsyncIterator[bytes],
87
- content_type: str,
88
- metadata: dict[str, Any] | None = None,
89
- ) -> str:
90
- """Upload content to S3."""
91
- try:
92
- session = self._get_session()
93
-
94
- # Prepare upload parameters
95
- upload_params = {
96
- "Bucket": self.bucket,
97
- "Key": key,
98
- "ContentType": content_type,
99
- **self.upload_config,
100
- }
101
-
102
- # Add custom metadata (S3 requires x-amz-meta- prefix)
103
- if metadata:
104
- s3_metadata = {}
105
- for k, v in metadata.items():
106
- # Convert values to strings and sanitize keys
107
- clean_key = k.replace("-", "_").replace(" ", "_")
108
- s3_metadata[clean_key] = str(v)
109
- upload_params["Metadata"] = s3_metadata
110
-
111
- # Handle streaming content for large files
112
- if isinstance(content, bytes):
113
- upload_params["Body"] = content
114
- else:
115
- # Collect streaming content into memory for upload
116
- # For very large files, consider using S3 multipart upload
117
- chunks = []
118
- total_size = 0
119
- async for chunk in content:
120
- chunks.append(chunk)
121
- total_size += len(chunk)
122
- # For files larger than 100MB, we could implement multipart upload
123
- if total_size > 100 * 1024 * 1024:
124
- logger.warning(
125
- f"Large file upload ({total_size} bytes) - "
126
- f"consider implementing multipart upload for key: {key}"
127
- )
128
-
129
- upload_params["Body"] = b"".join(chunks)
130
-
131
- # Upload using aioboto3
132
- async with session.client(
133
- "s3", config=self.config, endpoint_url=self.endpoint_url
134
- ) as s3:
135
- await s3.put_object(**upload_params)
136
-
137
- # Return the CloudFront URL if configured, otherwise S3 URL
138
- if self.cloudfront_domain:
139
- return f"https://{self.cloudfront_domain}/{key}"
140
- else:
141
- return f"https://{self.bucket}.s3.{self.region}.amazonaws.com/{key}"
142
-
143
- except Exception as e:
144
- if isinstance(e, StorageException):
145
- raise
146
- logger.error(f"Unexpected error uploading {key} to S3: {e}")
147
- raise StorageException(f"S3 upload failed: {e}") from e
148
-
149
- async def download(self, key: str) -> bytes:
150
- """Download file content from S3."""
151
- try:
152
- session = self._get_session()
153
- async with session.client(
154
- "s3", config=self.config, endpoint_url=self.endpoint_url
155
- ) as s3:
156
- response = await s3.get_object(Bucket=self.bucket, Key=key)
157
-
158
- # Read the streaming body
159
- content = await response["Body"].read()
160
- return content
161
-
162
- except Exception as e:
163
- if isinstance(e, StorageException):
164
- raise
165
- logger.error(f"Failed to download {key} from S3: {e}")
166
- raise StorageException(f"S3 download failed: {e}") from e
167
-
168
- async def get_presigned_upload_url(
169
- self,
170
- key: str,
171
- content_type: str,
172
- expires_in: timedelta | None = None,
173
- ) -> dict[str, Any]:
174
- """Generate presigned URL for direct client uploads."""
175
- if expires_in is None:
176
- expires_in = timedelta(hours=1)
177
-
178
- try:
179
- session = self._get_session()
180
- async with session.client(
181
- "s3", config=self.config, endpoint_url=self.endpoint_url
182
- ) as s3:
183
- # Generate presigned POST for direct uploads with form fields
184
- response = await s3.generate_presigned_post(
185
- Bucket=self.bucket,
186
- Key=key,
187
- Fields={"Content-Type": content_type, **self.upload_config},
188
- Conditions=[
189
- {"Content-Type": content_type},
190
- [
191
- "content-length-range",
192
- 1,
193
- self.upload_config.get("max_file_size", 100 * 1024 * 1024),
194
- ],
195
- ],
196
- ExpiresIn=int(expires_in.total_seconds()),
197
- )
198
-
199
- return {
200
- "url": response["url"],
201
- "fields": response["fields"],
202
- "expires_at": (datetime.now(UTC) + expires_in).isoformat(),
203
- }
204
-
205
- except Exception as e:
206
- if isinstance(e, StorageException):
207
- raise
208
- logger.error(f"Failed to create presigned upload URL for {key}: {e}")
209
- raise StorageException(f"S3 presigned URL creation failed: {e}") from e
210
-
211
- async def get_presigned_download_url(
212
- self, key: str, expires_in: timedelta | None = None
213
- ) -> str:
214
- """Generate presigned URL for secure downloads."""
215
- if expires_in is None:
216
- expires_in = timedelta(hours=1)
217
-
218
- try:
219
- # Always use S3 native presigned URLs for security
220
- session = self._get_session()
221
- async with session.client(
222
- "s3", config=self.config, endpoint_url=self.endpoint_url
223
- ) as s3:
224
- url = await s3.generate_presigned_url(
225
- "get_object",
226
- Params={"Bucket": self.bucket, "Key": key},
227
- ExpiresIn=int(expires_in.total_seconds()),
228
- )
229
- return url
230
-
231
- except Exception as e:
232
- if isinstance(e, StorageException):
233
- raise
234
- logger.error(f"Failed to create presigned download URL for {key}: {e}")
235
- raise StorageException(f"S3 presigned download URL creation failed: {e}") from e
236
-
237
- async def delete(self, key: str) -> bool:
238
- """Delete file by storage key."""
239
- try:
240
- session = self._get_session()
241
- async with session.client(
242
- "s3", config=self.config, endpoint_url=self.endpoint_url
243
- ) as s3:
244
- await s3.delete_object(Bucket=self.bucket, Key=key)
245
- return True
246
-
247
- except Exception as e:
248
- logger.error(f"Unexpected error deleting {key} from S3: {e}")
249
- raise StorageException(f"S3 delete failed: {e}") from e
250
-
251
- async def exists(self, key: str) -> bool:
252
- """Check if file exists."""
253
- try:
254
- session = self._get_session()
255
- async with session.client(
256
- "s3", config=self.config, endpoint_url=self.endpoint_url
257
- ) as s3:
258
- await s3.head_object(Bucket=self.bucket, Key=key)
259
- return True
260
- except Exception:
261
- return False
262
-
263
- async def get_metadata(self, key: str) -> dict[str, Any]:
264
- """Get file metadata (size, modified date, etc.)."""
265
- try:
266
- session = self._get_session()
267
- async with session.client(
268
- "s3", config=self.config, endpoint_url=self.endpoint_url
269
- ) as s3:
270
- response = await s3.head_object(Bucket=self.bucket, Key=key)
271
-
272
- # Extract metadata
273
- result = {
274
- "size": response.get("ContentLength", 0),
275
- "last_modified": response.get("LastModified"),
276
- "content_type": response.get("ContentType"),
277
- "etag": response.get("ETag", "").strip('"'),
278
- "version_id": response.get("VersionId"),
279
- "storage_class": response.get("StorageClass", "STANDARD"),
280
- "server_side_encryption": response.get("ServerSideEncryption"),
281
- }
282
-
283
- # Add custom metadata (remove x-amz-meta- prefix)
284
- custom_metadata = response.get("Metadata", {})
285
- if custom_metadata:
286
- result["custom_metadata"] = custom_metadata
287
-
288
- return result
289
-
290
- except Exception as e:
291
- if isinstance(e, StorageException):
292
- raise
293
- logger.error(f"Failed to get metadata for {key} from S3: {e}")
294
- raise StorageException(f"S3 get metadata failed: {e}") from e