@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,288 +0,0 @@
|
|
|
1
|
-
"""Factory for creating storage providers and managers."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from ..logging import get_logger
|
|
7
|
-
from .base import StorageManager, StorageProvider
|
|
8
|
-
from .config import StorageConfig, load_storage_config
|
|
9
|
-
from .implementations.local import LocalStorageProvider
|
|
10
|
-
|
|
11
|
-
logger = get_logger(__name__)
|
|
12
|
-
|
|
13
|
-
# Singleton storage configuration
|
|
14
|
-
# Loaded once at module import time to avoid re-parsing YAML on every request
|
|
15
|
-
_storage_config: StorageConfig | None = None
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_storage_config() -> StorageConfig:
|
|
19
|
-
"""Get the singleton storage configuration.
|
|
20
|
-
|
|
21
|
-
Loads the configuration from settings.storage_config_path on first access.
|
|
22
|
-
Subsequent calls return the cached configuration.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
StorageConfig instance
|
|
26
|
-
"""
|
|
27
|
-
global _storage_config
|
|
28
|
-
|
|
29
|
-
if _storage_config is None:
|
|
30
|
-
from ..config import settings
|
|
31
|
-
|
|
32
|
-
config_path = Path(settings.storage_config_path) if settings.storage_config_path else None
|
|
33
|
-
_storage_config = load_storage_config(config_path)
|
|
34
|
-
logger.info(
|
|
35
|
-
f"Loaded storage configuration: default_provider={_storage_config.default_provider}, "
|
|
36
|
-
f"providers={list(_storage_config.providers.keys())}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
return _storage_config
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Optional imports for cloud providers
|
|
43
|
-
try:
|
|
44
|
-
from .implementations.supabase import SupabaseStorageProvider
|
|
45
|
-
|
|
46
|
-
_supabase_available = True
|
|
47
|
-
except ImportError:
|
|
48
|
-
SupabaseStorageProvider = None
|
|
49
|
-
_supabase_available = False
|
|
50
|
-
logger.warning(
|
|
51
|
-
"Supabase storage not available. "
|
|
52
|
-
"Install with: pip install weirdfingers-boards[storage-supabase]"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
from .implementations.s3 import S3StorageProvider
|
|
57
|
-
|
|
58
|
-
_s3_available = True
|
|
59
|
-
except ImportError:
|
|
60
|
-
S3StorageProvider = None
|
|
61
|
-
_s3_available = False
|
|
62
|
-
logger.warning(
|
|
63
|
-
"S3 storage not available. Install with: pip install weirdfingers-boards[storage-s3]"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
from .implementations.gcs import GCSStorageProvider
|
|
68
|
-
|
|
69
|
-
_gcs_available = True
|
|
70
|
-
except ImportError:
|
|
71
|
-
GCSStorageProvider = None
|
|
72
|
-
_gcs_available = False
|
|
73
|
-
logger.warning(
|
|
74
|
-
"GCS storage not available. Install with: pip install weirdfingers-boards[storage-gcs]"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def create_storage_provider(provider_type: str, config: dict[str, Any]) -> StorageProvider:
|
|
79
|
-
"""Create a storage provider instance from configuration.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
provider_type: Type of provider ('local', 'supabase', 's3')
|
|
83
|
-
config: Provider configuration dictionary
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
StorageProvider instance
|
|
87
|
-
|
|
88
|
-
Raises:
|
|
89
|
-
ValueError: If provider type is unknown or configuration is invalid
|
|
90
|
-
ImportError: If required dependencies are not available
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
if provider_type == "local":
|
|
94
|
-
return _create_local_provider(config)
|
|
95
|
-
elif provider_type == "supabase":
|
|
96
|
-
if not _supabase_available:
|
|
97
|
-
raise ImportError(
|
|
98
|
-
"Supabase storage requires additional dependencies. "
|
|
99
|
-
"Install with: pip install weirdfingers-boards[storage-supabase]"
|
|
100
|
-
)
|
|
101
|
-
return _create_supabase_provider(config)
|
|
102
|
-
elif provider_type == "s3":
|
|
103
|
-
if not _s3_available:
|
|
104
|
-
raise ImportError(
|
|
105
|
-
"S3 storage requires additional dependencies. "
|
|
106
|
-
"Install with: pip install weirdfingers-boards[storage-s3]"
|
|
107
|
-
)
|
|
108
|
-
return _create_s3_provider(config)
|
|
109
|
-
elif provider_type == "gcs":
|
|
110
|
-
if not _gcs_available:
|
|
111
|
-
raise ImportError(
|
|
112
|
-
"GCS storage requires additional dependencies. "
|
|
113
|
-
"Install with: pip install weirdfingers-boards[storage-gcs]"
|
|
114
|
-
)
|
|
115
|
-
return _create_gcs_provider(config)
|
|
116
|
-
else:
|
|
117
|
-
raise ValueError(f"Unknown storage provider type: {provider_type}")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _create_local_provider(config: dict[str, Any]) -> LocalStorageProvider:
|
|
121
|
-
"""Create local storage provider."""
|
|
122
|
-
base_path = config.get("base_path", "/tmp/boards/storage")
|
|
123
|
-
public_url_base = config.get("public_url_base")
|
|
124
|
-
|
|
125
|
-
return LocalStorageProvider(base_path=Path(base_path), public_url_base=public_url_base)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _create_supabase_provider(config: dict[str, Any]) -> StorageProvider:
|
|
129
|
-
"""Create Supabase storage provider."""
|
|
130
|
-
if SupabaseStorageProvider is None:
|
|
131
|
-
raise ImportError("Supabase storage not available")
|
|
132
|
-
|
|
133
|
-
url = config.get("url")
|
|
134
|
-
key = config.get("key")
|
|
135
|
-
bucket = config.get("bucket", "boards-artifacts")
|
|
136
|
-
|
|
137
|
-
if not url:
|
|
138
|
-
raise ValueError("Supabase storage requires 'url' in configuration")
|
|
139
|
-
if not key:
|
|
140
|
-
raise ValueError("Supabase storage requires 'key' in configuration")
|
|
141
|
-
|
|
142
|
-
return SupabaseStorageProvider(url=url, key=key, bucket=bucket)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _create_s3_provider(config: dict[str, Any]) -> StorageProvider:
|
|
146
|
-
"""Create S3 storage provider."""
|
|
147
|
-
if S3StorageProvider is None:
|
|
148
|
-
raise ImportError("S3 storage not available")
|
|
149
|
-
|
|
150
|
-
bucket = config.get("bucket")
|
|
151
|
-
if not bucket:
|
|
152
|
-
raise ValueError("S3 storage requires 'bucket' in configuration")
|
|
153
|
-
|
|
154
|
-
region = config.get("region", "us-east-1")
|
|
155
|
-
aws_access_key_id = config.get("aws_access_key_id")
|
|
156
|
-
aws_secret_access_key = config.get("aws_secret_access_key")
|
|
157
|
-
aws_session_token = config.get("aws_session_token")
|
|
158
|
-
endpoint_url = config.get("endpoint_url")
|
|
159
|
-
cloudfront_domain = config.get("cloudfront_domain")
|
|
160
|
-
upload_config = config.get("upload_config", {})
|
|
161
|
-
|
|
162
|
-
return S3StorageProvider(
|
|
163
|
-
bucket=bucket,
|
|
164
|
-
region=region,
|
|
165
|
-
aws_access_key_id=aws_access_key_id,
|
|
166
|
-
aws_secret_access_key=aws_secret_access_key,
|
|
167
|
-
aws_session_token=aws_session_token,
|
|
168
|
-
endpoint_url=endpoint_url,
|
|
169
|
-
cloudfront_domain=cloudfront_domain,
|
|
170
|
-
upload_config=upload_config,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _create_gcs_provider(config: dict[str, Any]) -> StorageProvider:
|
|
175
|
-
"""Create GCS storage provider."""
|
|
176
|
-
if GCSStorageProvider is None:
|
|
177
|
-
raise ImportError("GCS storage not available")
|
|
178
|
-
|
|
179
|
-
bucket = config.get("bucket")
|
|
180
|
-
if not bucket:
|
|
181
|
-
raise ValueError("GCS storage requires 'bucket' in configuration")
|
|
182
|
-
|
|
183
|
-
project_id = config.get("project_id")
|
|
184
|
-
credentials_path = config.get("credentials_path")
|
|
185
|
-
credentials_json = config.get("credentials_json")
|
|
186
|
-
cdn_domain = config.get("cdn_domain")
|
|
187
|
-
upload_config = config.get("upload_config", {})
|
|
188
|
-
|
|
189
|
-
return GCSStorageProvider(
|
|
190
|
-
bucket=bucket,
|
|
191
|
-
project_id=project_id,
|
|
192
|
-
credentials_path=credentials_path,
|
|
193
|
-
credentials_json=credentials_json,
|
|
194
|
-
cdn_domain=cdn_domain,
|
|
195
|
-
upload_config=upload_config,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def _build_storage_manager_from_config(storage_config: StorageConfig) -> StorageManager:
|
|
200
|
-
"""Build a storage manager from a StorageConfig, registering all providers.
|
|
201
|
-
|
|
202
|
-
This is an internal helper that can be used for testing.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
storage_config: Storage configuration
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
StorageManager instance with registered providers
|
|
209
|
-
|
|
210
|
-
Raises:
|
|
211
|
-
RuntimeError: If no storage providers were successfully registered
|
|
212
|
-
"""
|
|
213
|
-
# Create storage manager
|
|
214
|
-
manager = StorageManager(storage_config)
|
|
215
|
-
|
|
216
|
-
# Register providers
|
|
217
|
-
for provider_name, provider_config in storage_config.providers.items():
|
|
218
|
-
try:
|
|
219
|
-
provider_type = provider_config.get("type", provider_name)
|
|
220
|
-
provider_instance = create_storage_provider(
|
|
221
|
-
provider_type, provider_config.get("config", {})
|
|
222
|
-
)
|
|
223
|
-
manager.register_provider(provider_name, provider_instance)
|
|
224
|
-
|
|
225
|
-
logger.info(f"Registered storage provider: {provider_name} ({provider_type})")
|
|
226
|
-
|
|
227
|
-
except Exception as e:
|
|
228
|
-
logger.error(f"Failed to register provider {provider_name}: {e}")
|
|
229
|
-
# Continue with other providers rather than failing completely
|
|
230
|
-
continue
|
|
231
|
-
|
|
232
|
-
# Validate default provider is available
|
|
233
|
-
if storage_config.default_provider not in manager.providers:
|
|
234
|
-
available = list(manager.providers.keys())
|
|
235
|
-
if not available:
|
|
236
|
-
raise RuntimeError("No storage providers were successfully registered")
|
|
237
|
-
|
|
238
|
-
logger.warning(
|
|
239
|
-
f"Default provider '{storage_config.default_provider}' not available. "
|
|
240
|
-
f"Using '{available[0]}' instead."
|
|
241
|
-
)
|
|
242
|
-
manager.default_provider = available[0]
|
|
243
|
-
|
|
244
|
-
return manager
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def create_storage_manager() -> StorageManager:
|
|
248
|
-
"""Create a configured storage manager using global singleton config.
|
|
249
|
-
|
|
250
|
-
The storage configuration is loaded once from settings.storage_config_path
|
|
251
|
-
and cached for the lifetime of the process.
|
|
252
|
-
|
|
253
|
-
Returns:
|
|
254
|
-
StorageManager instance with registered providers
|
|
255
|
-
"""
|
|
256
|
-
storage_config = get_storage_config()
|
|
257
|
-
return _build_storage_manager_from_config(storage_config)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def create_development_storage() -> StorageManager:
|
|
261
|
-
"""Create a simple storage manager for development use.
|
|
262
|
-
|
|
263
|
-
Uses local filesystem storage with sensible defaults.
|
|
264
|
-
This is primarily used for testing and creates a standalone manager
|
|
265
|
-
rather than using global settings.
|
|
266
|
-
"""
|
|
267
|
-
config = StorageConfig(
|
|
268
|
-
default_provider="local",
|
|
269
|
-
providers={
|
|
270
|
-
"local": {
|
|
271
|
-
"type": "local",
|
|
272
|
-
"config": {
|
|
273
|
-
"base_path": "/tmp/boards/storage",
|
|
274
|
-
"public_url_base": "http://localhost:8088/api/storage",
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
routing_rules=[{"provider": "local"}],
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
# Create storage manager directly without using global settings
|
|
282
|
-
manager = StorageManager(config)
|
|
283
|
-
|
|
284
|
-
# Register the local provider
|
|
285
|
-
local_provider = create_storage_provider("local", config.providers["local"]["config"])
|
|
286
|
-
manager.register_provider("local", local_provider)
|
|
287
|
-
|
|
288
|
-
return manager
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""Storage provider implementations."""
|
|
2
|
-
|
|
3
|
-
from .local import LocalStorageProvider
|
|
4
|
-
|
|
5
|
-
# Optional cloud providers - imported conditionally to avoid import errors
|
|
6
|
-
__all__ = ["LocalStorageProvider"]
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
from .supabase import SupabaseStorageProvider
|
|
10
|
-
|
|
11
|
-
__all__.append("SupabaseStorageProvider")
|
|
12
|
-
except ImportError:
|
|
13
|
-
pass
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
from .s3 import S3StorageProvider
|
|
17
|
-
|
|
18
|
-
__all__.append("S3StorageProvider")
|
|
19
|
-
except ImportError:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from .gcs import GCSStorageProvider
|
|
24
|
-
|
|
25
|
-
__all__.append("GCSStorageProvider")
|
|
26
|
-
except ImportError:
|
|
27
|
-
pass
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
"""Google Cloud Storage provider with IAM auth and CDN support."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from collections.abc import AsyncIterator
|
|
6
|
-
from datetime import UTC, datetime, timedelta
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from google.cloud import storage
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
import asyncio
|
|
15
|
-
|
|
16
|
-
from google.auth import default
|
|
17
|
-
from google.auth.exceptions import DefaultCredentialsError
|
|
18
|
-
from google.cloud import storage
|
|
19
|
-
from google.cloud.exceptions import GoogleCloudError, NotFound
|
|
20
|
-
|
|
21
|
-
_gcs_available = True
|
|
22
|
-
except ImportError:
|
|
23
|
-
storage = None
|
|
24
|
-
NotFound = None
|
|
25
|
-
GoogleCloudError = None
|
|
26
|
-
default = None
|
|
27
|
-
DefaultCredentialsError = None
|
|
28
|
-
_gcs_available = False
|
|
29
|
-
|
|
30
|
-
from ...logging import get_logger
|
|
31
|
-
from ..base import StorageException, StorageProvider
|
|
32
|
-
|
|
33
|
-
logger = get_logger(__name__)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class GCSStorageProvider(StorageProvider):
|
|
37
|
-
"""Google Cloud Storage with IAM auth, Cloud CDN, and proper async patterns."""
|
|
38
|
-
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
bucket: str,
|
|
42
|
-
project_id: str | None = None,
|
|
43
|
-
credentials_path: str | None = None,
|
|
44
|
-
credentials_json: str | None = None,
|
|
45
|
-
cdn_domain: str | None = None,
|
|
46
|
-
upload_config: dict[str, Any] | None = None,
|
|
47
|
-
):
|
|
48
|
-
if not _gcs_available:
|
|
49
|
-
raise ImportError(
|
|
50
|
-
"google-cloud-storage is required for GCSStorageProvider. "
|
|
51
|
-
"Install with: pip install google-cloud-storage"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
self.bucket_name = bucket
|
|
55
|
-
self.project_id = project_id
|
|
56
|
-
self.credentials_path = credentials_path
|
|
57
|
-
self.credentials_json = credentials_json
|
|
58
|
-
self.cdn_domain = cdn_domain
|
|
59
|
-
|
|
60
|
-
# Default upload configuration
|
|
61
|
-
self.upload_config = {
|
|
62
|
-
"cache_control": "public, max-age=3600",
|
|
63
|
-
"predefined_acl": None, # Use bucket's default ACL
|
|
64
|
-
**(upload_config or {}),
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
self._client: Any | None = None
|
|
68
|
-
self._bucket: Any | None = None
|
|
69
|
-
|
|
70
|
-
# Client will be initialized lazily on first use
|
|
71
|
-
|
|
72
|
-
def _get_client(self) -> Any:
|
|
73
|
-
"""Get or create the GCS client with proper authentication."""
|
|
74
|
-
if self._client is None:
|
|
75
|
-
if storage is None:
|
|
76
|
-
raise ImportError("google-cloud-storage is required for GCSStorageProvider")
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
if self.credentials_json:
|
|
80
|
-
# Use JSON credentials string
|
|
81
|
-
credentials_info = json.loads(self.credentials_json)
|
|
82
|
-
from google.oauth2 import service_account
|
|
83
|
-
|
|
84
|
-
credentials = service_account.Credentials.from_service_account_info(
|
|
85
|
-
credentials_info,
|
|
86
|
-
scopes=["https://www.googleapis.com/auth/cloud-platform"],
|
|
87
|
-
)
|
|
88
|
-
self._client = storage.Client(credentials=credentials, project=self.project_id)
|
|
89
|
-
elif self.credentials_path:
|
|
90
|
-
# Use service account file
|
|
91
|
-
credentials_path = Path(self.credentials_path)
|
|
92
|
-
if not credentials_path.exists():
|
|
93
|
-
raise FileNotFoundError(
|
|
94
|
-
f"Credentials file not found: {self.credentials_path}"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(credentials_path)
|
|
98
|
-
self._client = storage.Client(project=self.project_id)
|
|
99
|
-
else:
|
|
100
|
-
# Use default credentials (environment variables, gcloud, etc.)
|
|
101
|
-
self._client = storage.Client(project=self.project_id)
|
|
102
|
-
|
|
103
|
-
# Get bucket reference
|
|
104
|
-
self._bucket = self._client.bucket(self.bucket_name)
|
|
105
|
-
|
|
106
|
-
except Exception as e:
|
|
107
|
-
logger.error(f"Failed to initialize GCS client: {e}")
|
|
108
|
-
raise StorageException(f"GCS client initialization failed: {e}") from e
|
|
109
|
-
|
|
110
|
-
return self._client
|
|
111
|
-
|
|
112
|
-
async def _run_sync(self, func, *args, **kwargs) -> Any:
|
|
113
|
-
"""Run synchronous GCS operations in thread pool."""
|
|
114
|
-
loop = asyncio.get_event_loop()
|
|
115
|
-
return await loop.run_in_executor(None, func, *args, **kwargs)
|
|
116
|
-
|
|
117
|
-
async def upload(
|
|
118
|
-
self,
|
|
119
|
-
key: str,
|
|
120
|
-
content: bytes | AsyncIterator[bytes],
|
|
121
|
-
content_type: str,
|
|
122
|
-
metadata: dict[str, Any] | None = None,
|
|
123
|
-
) -> str:
|
|
124
|
-
"""Upload content to GCS."""
|
|
125
|
-
try:
|
|
126
|
-
# Get client (initializes on first use)
|
|
127
|
-
client = self._get_client()
|
|
128
|
-
bucket = client.bucket(self.bucket_name)
|
|
129
|
-
|
|
130
|
-
# Create blob object
|
|
131
|
-
blob = bucket.blob(key)
|
|
132
|
-
|
|
133
|
-
# Set content type
|
|
134
|
-
blob.content_type = content_type
|
|
135
|
-
|
|
136
|
-
# Set cache control and other configuration
|
|
137
|
-
if self.upload_config.get("cache_control"):
|
|
138
|
-
blob.cache_control = self.upload_config["cache_control"]
|
|
139
|
-
|
|
140
|
-
# Add custom metadata
|
|
141
|
-
if metadata:
|
|
142
|
-
# GCS metadata keys must be lowercase and can contain only letters,
|
|
143
|
-
# numbers, and underscores
|
|
144
|
-
gcs_metadata = {}
|
|
145
|
-
for k, v in metadata.items():
|
|
146
|
-
# Convert key to lowercase and replace invalid characters
|
|
147
|
-
clean_key = k.lower().replace("-", "_").replace(" ", "_")
|
|
148
|
-
gcs_metadata[clean_key] = str(v)
|
|
149
|
-
blob.metadata = gcs_metadata
|
|
150
|
-
|
|
151
|
-
# Handle streaming content for large files
|
|
152
|
-
if isinstance(content, bytes):
|
|
153
|
-
file_content = content
|
|
154
|
-
else:
|
|
155
|
-
# Collect streaming content into memory for upload
|
|
156
|
-
# For very large files, consider using resumable uploads
|
|
157
|
-
chunks = []
|
|
158
|
-
total_size = 0
|
|
159
|
-
async for chunk in content:
|
|
160
|
-
chunks.append(chunk)
|
|
161
|
-
total_size += len(chunk)
|
|
162
|
-
# For files larger than 100MB, we could implement resumable upload
|
|
163
|
-
if total_size > 100 * 1024 * 1024:
|
|
164
|
-
logger.warning(
|
|
165
|
-
f"Large file upload ({total_size} bytes) - "
|
|
166
|
-
f"consider implementing resumable upload for key: {key}"
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
file_content = b"".join(chunks)
|
|
170
|
-
|
|
171
|
-
# Upload using thread pool to avoid blocking
|
|
172
|
-
await self._run_sync(blob.upload_from_string, file_content, content_type=content_type)
|
|
173
|
-
|
|
174
|
-
# Return the CDN URL if configured, otherwise public GCS URL
|
|
175
|
-
if self.cdn_domain:
|
|
176
|
-
return f"https://{self.cdn_domain}/{key}"
|
|
177
|
-
else:
|
|
178
|
-
return f"https://storage.googleapis.com/{self.bucket_name}/{key}"
|
|
179
|
-
|
|
180
|
-
except Exception as e:
|
|
181
|
-
if isinstance(e, StorageException):
|
|
182
|
-
raise
|
|
183
|
-
logger.error(f"Unexpected error uploading {key} to GCS: {e}")
|
|
184
|
-
raise StorageException(f"GCS upload failed: {e}") from e
|
|
185
|
-
|
|
186
|
-
async def download(self, key: str) -> bytes:
|
|
187
|
-
"""Download file content from GCS."""
|
|
188
|
-
try:
|
|
189
|
-
# Get client (initializes on first use)
|
|
190
|
-
client = self._get_client()
|
|
191
|
-
bucket = client.bucket(self.bucket_name)
|
|
192
|
-
|
|
193
|
-
blob = bucket.blob(key)
|
|
194
|
-
|
|
195
|
-
# Download using thread pool to avoid blocking
|
|
196
|
-
content = await self._run_sync(blob.download_as_bytes)
|
|
197
|
-
return content
|
|
198
|
-
|
|
199
|
-
except Exception as e:
|
|
200
|
-
if isinstance(e, StorageException):
|
|
201
|
-
raise
|
|
202
|
-
logger.error(f"Failed to download {key} from GCS: {e}")
|
|
203
|
-
raise StorageException(f"GCS download failed: {e}") from e
|
|
204
|
-
|
|
205
|
-
async def get_presigned_upload_url(
|
|
206
|
-
self,
|
|
207
|
-
key: str,
|
|
208
|
-
content_type: str,
|
|
209
|
-
expires_in: timedelta | None = None,
|
|
210
|
-
) -> dict[str, Any]:
|
|
211
|
-
"""Generate presigned URL for direct client uploads."""
|
|
212
|
-
if expires_in is None:
|
|
213
|
-
expires_in = timedelta(hours=1)
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
# Get client (initializes on first use)
|
|
217
|
-
client = self._get_client()
|
|
218
|
-
bucket = client.bucket(self.bucket_name)
|
|
219
|
-
|
|
220
|
-
blob = bucket.blob(key)
|
|
221
|
-
|
|
222
|
-
# Generate signed URL for PUT operations
|
|
223
|
-
url = await self._run_sync(
|
|
224
|
-
blob.generate_signed_url,
|
|
225
|
-
version="v4",
|
|
226
|
-
expiration=expires_in,
|
|
227
|
-
method="PUT",
|
|
228
|
-
content_type=content_type,
|
|
229
|
-
headers={"Content-Type": content_type},
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
"url": url,
|
|
234
|
-
"method": "PUT",
|
|
235
|
-
"headers": {"Content-Type": content_type},
|
|
236
|
-
"expires_at": (datetime.now(UTC) + expires_in).isoformat(),
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
except Exception as e:
|
|
240
|
-
if isinstance(e, StorageException):
|
|
241
|
-
raise
|
|
242
|
-
logger.error(f"Failed to create presigned upload URL for {key}: {e}")
|
|
243
|
-
raise StorageException(f"GCS presigned URL creation failed: {e}") from e
|
|
244
|
-
|
|
245
|
-
async def get_presigned_download_url(
|
|
246
|
-
self, key: str, expires_in: timedelta | None = None
|
|
247
|
-
) -> str:
|
|
248
|
-
"""Generate presigned URL for secure downloads."""
|
|
249
|
-
if expires_in is None:
|
|
250
|
-
expires_in = timedelta(hours=1)
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
# Always use GCS native signed URLs for security
|
|
254
|
-
# Get client (initializes on first use)
|
|
255
|
-
client = self._get_client()
|
|
256
|
-
bucket = client.bucket(self.bucket_name)
|
|
257
|
-
|
|
258
|
-
blob = bucket.blob(key)
|
|
259
|
-
|
|
260
|
-
# Generate signed URL for GET operations
|
|
261
|
-
url = await self._run_sync(
|
|
262
|
-
blob.generate_signed_url,
|
|
263
|
-
version="v4",
|
|
264
|
-
expiration=expires_in,
|
|
265
|
-
method="GET",
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
return url
|
|
269
|
-
|
|
270
|
-
except Exception as e:
|
|
271
|
-
if isinstance(e, StorageException):
|
|
272
|
-
raise
|
|
273
|
-
logger.error(f"Failed to create presigned download URL for {key}: {e}")
|
|
274
|
-
raise StorageException(f"GCS presigned download URL creation failed: {e}") from e
|
|
275
|
-
|
|
276
|
-
async def delete(self, key: str) -> bool:
|
|
277
|
-
"""Delete file by storage key."""
|
|
278
|
-
try:
|
|
279
|
-
# Get client (initializes on first use)
|
|
280
|
-
client = self._get_client()
|
|
281
|
-
bucket = client.bucket(self.bucket_name)
|
|
282
|
-
|
|
283
|
-
blob = bucket.blob(key)
|
|
284
|
-
await self._run_sync(blob.delete)
|
|
285
|
-
return True
|
|
286
|
-
|
|
287
|
-
except Exception as e:
|
|
288
|
-
logger.error(f"Unexpected error deleting {key} from GCS: {e}")
|
|
289
|
-
raise StorageException(f"GCS delete failed: {e}") from e
|
|
290
|
-
|
|
291
|
-
async def exists(self, key: str) -> bool:
|
|
292
|
-
"""Check if file exists."""
|
|
293
|
-
try:
|
|
294
|
-
# Get client (initializes on first use)
|
|
295
|
-
client = self._get_client()
|
|
296
|
-
bucket = client.bucket(self.bucket_name)
|
|
297
|
-
|
|
298
|
-
blob = bucket.blob(key)
|
|
299
|
-
exists = await self._run_sync(blob.exists)
|
|
300
|
-
return exists
|
|
301
|
-
|
|
302
|
-
except Exception:
|
|
303
|
-
return False
|
|
304
|
-
|
|
305
|
-
async def get_metadata(self, key: str) -> dict[str, Any]:
|
|
306
|
-
"""Get file metadata (size, modified date, etc.)."""
|
|
307
|
-
try:
|
|
308
|
-
# Get client (initializes on first use)
|
|
309
|
-
client = self._get_client()
|
|
310
|
-
bucket = client.bucket(self.bucket_name)
|
|
311
|
-
|
|
312
|
-
blob = bucket.blob(key)
|
|
313
|
-
|
|
314
|
-
# Reload blob to get latest metadata
|
|
315
|
-
await self._run_sync(blob.reload)
|
|
316
|
-
|
|
317
|
-
result = {
|
|
318
|
-
"size": blob.size or 0,
|
|
319
|
-
"last_modified": blob.updated,
|
|
320
|
-
"content_type": blob.content_type,
|
|
321
|
-
"etag": blob.etag,
|
|
322
|
-
"generation": blob.generation,
|
|
323
|
-
"storage_class": blob.storage_class,
|
|
324
|
-
"cache_control": blob.cache_control,
|
|
325
|
-
"content_encoding": blob.content_encoding,
|
|
326
|
-
"content_disposition": blob.content_disposition,
|
|
327
|
-
"content_language": blob.content_language,
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# Add custom metadata
|
|
331
|
-
if blob.metadata:
|
|
332
|
-
result["custom_metadata"] = blob.metadata
|
|
333
|
-
|
|
334
|
-
return result
|
|
335
|
-
|
|
336
|
-
except Exception as e:
|
|
337
|
-
if isinstance(e, StorageException):
|
|
338
|
-
raise
|
|
339
|
-
logger.error(f"Failed to get metadata for {key} from GCS: {e}")
|
|
340
|
-
raise StorageException(f"GCS get metadata failed: {e}") from e
|