@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.
- package/dist/index.js +561 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- 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)
|