morphosx 0.4.0__tar.gz

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.
morphosx-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Di Criscito
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.3
2
+ Name: morphosx
3
+ Version: 0.4.0
4
+ Summary: high-performance media processing engine for on-the-fly image transformations and storage.
5
+ License: MIT
6
+ Keywords: media,image-processing,cdn,fastapi,bim,ifc,3d,on-the-fly
7
+ Author: Davide Di Criscito
8
+ Requires-Python: >=3.11,<3.15
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
14
+ Provides-Extra: 3d
15
+ Provides-Extra: bim
16
+ Provides-Extra: full
17
+ Provides-Extra: modern
18
+ Provides-Extra: office
19
+ Provides-Extra: pdf
20
+ Provides-Extra: raw
21
+ Provides-Extra: video
22
+ Provides-Extra: vips
23
+ Requires-Dist: aioboto3 (>=15.5.0,<16.0.0)
24
+ Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
25
+ Requires-Dist: fastapi (>=0.132.0,<0.133.0)
26
+ Requires-Dist: ffmpeg-python (>=0.2.0,<0.3.0) ; extra == "video"
27
+ Requires-Dist: ffmpeg-python ; extra == "full"
28
+ Requires-Dist: ifcopenshell (>=0.8.4.post1,<0.9.0) ; extra == "bim"
29
+ Requires-Dist: ifcopenshell ; extra == "full"
30
+ Requires-Dist: imageio (>=2.37.2,<3.0.0) ; extra == "raw"
31
+ Requires-Dist: imageio ; extra == "full"
32
+ Requires-Dist: markdown (>=3.10.2,<4.0.0)
33
+ Requires-Dist: numpy (<2.0.0) ; extra == "raw"
34
+ Requires-Dist: numpy ; extra == "full"
35
+ Requires-Dist: openpyxl (>=3.1.5,<4.0.0) ; extra == "office"
36
+ Requires-Dist: openpyxl ; extra == "full"
37
+ Requires-Dist: passlib[bcrypt] (>=1.7.4,<2.0.0)
38
+ Requires-Dist: pillow (>=12.1.1,<13.0.0)
39
+ Requires-Dist: pillow-avif-plugin (>=1.5.5,<2.0.0) ; extra == "modern"
40
+ Requires-Dist: pillow-avif-plugin ; extra == "full"
41
+ Requires-Dist: pillow-heif (>=1.2.1,<2.0.0) ; extra == "modern"
42
+ Requires-Dist: pillow-heif ; extra == "full"
43
+ Requires-Dist: pydantic-settings (>=2.13.1,<3.0.0)
44
+ Requires-Dist: pygltflib (>=1.16.5,<2.0.0) ; extra == "3d"
45
+ Requires-Dist: pygltflib ; extra == "full"
46
+ Requires-Dist: pygments (>=2.19.2,<3.0.0)
47
+ Requires-Dist: pymupdf (>=1.27.1,<2.0.0) ; extra == "pdf"
48
+ Requires-Dist: pymupdf ; extra == "full"
49
+ Requires-Dist: python-docx (>=1.2.0,<2.0.0) ; extra == "office"
50
+ Requires-Dist: python-docx ; extra == "full"
51
+ Requires-Dist: python-jose[cryptography] (>=3.5.0,<4.0.0)
52
+ Requires-Dist: python-multipart (>=0.0.22,<0.0.23)
53
+ Requires-Dist: python-pptx (>=1.0.2,<2.0.0) ; extra == "office"
54
+ Requires-Dist: python-pptx ; extra == "full"
55
+ Requires-Dist: pyvips (>=3.1.1,<4.0.0) ; extra == "vips"
56
+ Requires-Dist: pyvips ; extra == "full"
57
+ Requires-Dist: rawpy (>=0.26.1,<0.27.0) ; extra == "raw"
58
+ Requires-Dist: rawpy ; extra == "full"
59
+ Requires-Dist: trimesh (>=4.11.2,<5.0.0) ; extra == "3d"
60
+ Requires-Dist: trimesh ; extra == "full"
61
+ Requires-Dist: uvicorn (>=0.41.0,<0.42.0)
62
+ Description-Content-Type: text/markdown
63
+
64
+ <p align="center">
65
+ <img src="morphosx-banner.png" alt="morphosx banner" width="600px">
66
+ </p>
67
+
68
+ # morphosx 🧬
69
+
70
+ > **High performance, low footprint.**
71
+ > Self-hosted, open-source media engine for on-the-fly image processing and delivery.
72
+
73
+ `morphosx` is a high-speed, minimal cloud storage and media manipulation server. It converts almost any media type into a optimized, web-ready image derivative on-the-fly.
74
+
75
+ ---
76
+
77
+ ## ⚡ Core Features
78
+
79
+ - **User-Bound Security**: Protected assets and HMAC signatures tied to specific **JWT-authenticated** users.
80
+ - **Private Folders**: Secure per-user storage using the `users/{user_id}/` path convention.
81
+ - **Universal Rendering**: Support for BIM (IFC), 3D (STL/OBJ/GLB), Office, Font Specimen, Archives, Video, Audio and RAW.
82
+ - **Modern Engines**: Choice between **Pillow** and **PyVips** (ultra-fast).
83
+ - **Cloud Ready**: Pluggable storage system (Local & **Amazon S3**).
84
+
85
+ ---
86
+
87
+ ## 🚀 Installation & Deployment
88
+
89
+ ### 1. Using Docker (Recommended)
90
+
91
+ The easiest way to run Morphosx with all features and system dependencies pre-installed.
92
+
93
+ ```bash
94
+ docker run -p 8000:8000 --env-file .env ghcr.io/dcdavidev/morphosx:latest
95
+ ```
96
+
97
+ ### 2. Using pip (from PyPI)
98
+
99
+ You can install Morphosx as a library or a standalone CLI tool.
100
+
101
+ ```bash
102
+ # Core installation (standard images only)
103
+ pip install morphosx
104
+
105
+ # Full installation (all media types support)
106
+ pip install "morphosx[full]"
107
+
108
+ # Selective installation
109
+ pip install "morphosx[video,pdf,3d]"
110
+ ```
111
+
112
+ **Note**: Some extras require system libraries (e.g., `ffmpeg` for video, `libvips` for vips engine).
113
+
114
+ ---
115
+
116
+ ## 📖 Usage Guide
117
+
118
+ ### Start the Server
119
+
120
+ If installed via pip, you can use the global command:
121
+
122
+ ```bash
123
+ morphosx start --port 8000 --reload
124
+ ```
125
+
126
+ ### 1. Uploading Assets
127
+
128
+ **Public Upload**
129
+
130
+ ```bash
131
+ curl -X POST "http://localhost:8000/v1/assets/upload?folder=news" -F "file=@img.jpg"
132
+ ```
133
+
134
+ **Private Upload**
135
+
136
+ ```bash
137
+ curl -X POST "http://localhost:8000/v1/assets/upload?private=true" \
138
+ -H "Authorization: Bearer <TOKEN>" -F "file=@secret.pdf"
139
+ ```
140
+
141
+ ### 2. Listing Files
142
+
143
+ ```text
144
+ GET /v1/assets/list/originals/news
145
+ ```
146
+
147
+ ---
148
+
149
+ ## ✨ Smart Presets
150
+
151
+ Use predefined aliases in `settings.py` for cleaner URLs:
152
+
153
+ - `preset=thumb`: 150x150 WebP.
154
+ - `preset=hero`: 1920px WebP.
155
+ - `preset=social`: 1200x630 JPEG.
156
+
157
+ ---
158
+
159
+ ## 🛡️ Advanced Security
160
+
161
+ Morphosx uses **HMAC-SHA256** to prevent DoS attacks.
162
+ The signature payload includes: `asset_id | width | height | format | quality | preset | user_id`.
163
+
164
+ ---
165
+
166
+ ## 🧪 Supported Media Table
167
+
168
+ | Category | Extra | Extensions | Output Type |
169
+ | :------------- | :--------- | :------------------ | :--------------------- |
170
+ | **BIM** | `[bim]` | ifc | Technical Project Card |
171
+ | **3D** | `[3d]` | stl, obj, glb, gltf | Technical Blueprint |
172
+ | **Images** | Core | jpg, png, webp | Processed Image |
173
+ | **Modern Img** | `[modern]` | heic, avif | Processed Image |
174
+ | **RAW** | `[raw]` | cr2, nef, dng, arw | Developed Image |
175
+ | **Video** | `[video]` | mp4, mov, webm, avi | Frame @ timestamp |
176
+ | **Audio** | `[video]` | mp3, wav, ogg, flac | Waveform Image |
177
+ | **Docs** | `[pdf]` | pdf | Page Render |
178
+ | **Office** | `[office]` | docx, pptx, xlsx | Summary Card |
179
+ | **Text** | Core | json, xml, md, txt | Syntax-highlighted |
180
+ | **Typography** | Core | ttf, otf | Font Specimen |
181
+ | **Archives** | Core | zip, tar | Content List |
182
+
183
+ ---
184
+
185
+ ## 📜 License
186
+
187
+ MIT - Built for the Open Source community.
188
+
@@ -0,0 +1,124 @@
1
+ <p align="center">
2
+ <img src="morphosx-banner.png" alt="morphosx banner" width="600px">
3
+ </p>
4
+
5
+ # morphosx 🧬
6
+
7
+ > **High performance, low footprint.**
8
+ > Self-hosted, open-source media engine for on-the-fly image processing and delivery.
9
+
10
+ `morphosx` is a high-speed, minimal cloud storage and media manipulation server. It converts almost any media type into a optimized, web-ready image derivative on-the-fly.
11
+
12
+ ---
13
+
14
+ ## ⚡ Core Features
15
+
16
+ - **User-Bound Security**: Protected assets and HMAC signatures tied to specific **JWT-authenticated** users.
17
+ - **Private Folders**: Secure per-user storage using the `users/{user_id}/` path convention.
18
+ - **Universal Rendering**: Support for BIM (IFC), 3D (STL/OBJ/GLB), Office, Font Specimen, Archives, Video, Audio and RAW.
19
+ - **Modern Engines**: Choice between **Pillow** and **PyVips** (ultra-fast).
20
+ - **Cloud Ready**: Pluggable storage system (Local & **Amazon S3**).
21
+
22
+ ---
23
+
24
+ ## 🚀 Installation & Deployment
25
+
26
+ ### 1. Using Docker (Recommended)
27
+
28
+ The easiest way to run Morphosx with all features and system dependencies pre-installed.
29
+
30
+ ```bash
31
+ docker run -p 8000:8000 --env-file .env ghcr.io/dcdavidev/morphosx:latest
32
+ ```
33
+
34
+ ### 2. Using pip (from PyPI)
35
+
36
+ You can install Morphosx as a library or a standalone CLI tool.
37
+
38
+ ```bash
39
+ # Core installation (standard images only)
40
+ pip install morphosx
41
+
42
+ # Full installation (all media types support)
43
+ pip install "morphosx[full]"
44
+
45
+ # Selective installation
46
+ pip install "morphosx[video,pdf,3d]"
47
+ ```
48
+
49
+ **Note**: Some extras require system libraries (e.g., `ffmpeg` for video, `libvips` for vips engine).
50
+
51
+ ---
52
+
53
+ ## 📖 Usage Guide
54
+
55
+ ### Start the Server
56
+
57
+ If installed via pip, you can use the global command:
58
+
59
+ ```bash
60
+ morphosx start --port 8000 --reload
61
+ ```
62
+
63
+ ### 1. Uploading Assets
64
+
65
+ **Public Upload**
66
+
67
+ ```bash
68
+ curl -X POST "http://localhost:8000/v1/assets/upload?folder=news" -F "file=@img.jpg"
69
+ ```
70
+
71
+ **Private Upload**
72
+
73
+ ```bash
74
+ curl -X POST "http://localhost:8000/v1/assets/upload?private=true" \
75
+ -H "Authorization: Bearer <TOKEN>" -F "file=@secret.pdf"
76
+ ```
77
+
78
+ ### 2. Listing Files
79
+
80
+ ```text
81
+ GET /v1/assets/list/originals/news
82
+ ```
83
+
84
+ ---
85
+
86
+ ## ✨ Smart Presets
87
+
88
+ Use predefined aliases in `settings.py` for cleaner URLs:
89
+
90
+ - `preset=thumb`: 150x150 WebP.
91
+ - `preset=hero`: 1920px WebP.
92
+ - `preset=social`: 1200x630 JPEG.
93
+
94
+ ---
95
+
96
+ ## 🛡️ Advanced Security
97
+
98
+ Morphosx uses **HMAC-SHA256** to prevent DoS attacks.
99
+ The signature payload includes: `asset_id | width | height | format | quality | preset | user_id`.
100
+
101
+ ---
102
+
103
+ ## 🧪 Supported Media Table
104
+
105
+ | Category | Extra | Extensions | Output Type |
106
+ | :------------- | :--------- | :------------------ | :--------------------- |
107
+ | **BIM** | `[bim]` | ifc | Technical Project Card |
108
+ | **3D** | `[3d]` | stl, obj, glb, gltf | Technical Blueprint |
109
+ | **Images** | Core | jpg, png, webp | Processed Image |
110
+ | **Modern Img** | `[modern]` | heic, avif | Processed Image |
111
+ | **RAW** | `[raw]` | cr2, nef, dng, arw | Developed Image |
112
+ | **Video** | `[video]` | mp4, mov, webm, avi | Frame @ timestamp |
113
+ | **Audio** | `[video]` | mp3, wav, ogg, flac | Waveform Image |
114
+ | **Docs** | `[pdf]` | pdf | Page Render |
115
+ | **Office** | `[office]` | docx, pptx, xlsx | Summary Card |
116
+ | **Text** | Core | json, xml, md, txt | Syntax-highlighted |
117
+ | **Typography** | Core | ttf, otf | Font Specimen |
118
+ | **Archives** | Core | zip, tar | Content List |
119
+
120
+ ---
121
+
122
+ ## 📜 License
123
+
124
+ MIT - Built for the Open Source community.
File without changes
File without changes
@@ -0,0 +1,346 @@
1
+ from pathlib import Path
2
+ import uuid
3
+ from mimetypes import guess_extension
4
+ from typing import Optional
5
+ from fastapi import APIRouter, Depends, HTTPException, Query, Response, UploadFile, File
6
+
7
+ from morphosx.app.core.security import verify_signature, generate_signature
8
+ from morphosx.app.core.auth import get_current_user
9
+ from morphosx.app.engine.processor import ImageProcessor, ProcessingOptions, ImageFormat
10
+ from morphosx.app.engine.video import VideoProcessor
11
+ from morphosx.app.engine.audio import AudioProcessor
12
+ from morphosx.app.engine.document import DocumentProcessor
13
+ from morphosx.app.engine.raw import RawProcessor
14
+ from morphosx.app.engine.vips import VipsProcessor
15
+ from morphosx.app.engine.text import TextProcessor
16
+ from morphosx.app.engine.office import OfficeProcessor
17
+ from morphosx.app.engine.font import FontProcessor
18
+ from morphosx.app.engine.model3d import Model3DProcessor
19
+ from morphosx.app.engine.archive import ArchiveProcessor
20
+ from morphosx.app.engine.bim import BIMProcessor
21
+ from morphosx.app.storage.local import LocalStorage
22
+ from morphosx.app.storage.s3 import S3Storage
23
+ from morphosx.app.settings import settings
24
+
25
+
26
+ router = APIRouter(prefix="/assets", tags=["Assets"])
27
+
28
+ # Singleton instances factory
29
+ def get_storage():
30
+ if settings.storage_type == "s3":
31
+ if not settings.s3_bucket:
32
+ raise ValueError("S3_BUCKET is required for s3 storage_type")
33
+ return S3Storage(
34
+ bucket_name=settings.s3_bucket,
35
+ region_name=settings.s3_region,
36
+ endpoint_url=settings.s3_endpoint,
37
+ access_key_id=settings.s3_access_key,
38
+ secret_access_key=settings.s3_secret_key
39
+ )
40
+ return LocalStorage(base_directory=settings.storage_root)
41
+
42
+ def get_processor():
43
+ if settings.engine_type == "vips":
44
+ return VipsProcessor()
45
+ return ImageProcessor()
46
+
47
+ storage = get_storage()
48
+ processor = get_processor()
49
+ video_processor = VideoProcessor()
50
+ audio_processor = AudioProcessor()
51
+ document_processor = DocumentProcessor()
52
+ raw_processor = RawProcessor()
53
+ text_processor = TextProcessor()
54
+ office_processor = OfficeProcessor()
55
+ font_processor = FontProcessor()
56
+ model3d_processor = Model3DProcessor()
57
+ archive_processor = ArchiveProcessor()
58
+ bim_processor = BIMProcessor()
59
+
60
+ VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".avi"}
61
+ AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac"}
62
+ DOCUMENT_EXTENSIONS = {".pdf"}
63
+ RAW_EXTENSIONS = {".cr2", ".nef", ".dng", ".arw"}
64
+ TEXT_EXTENSIONS = {".json", ".xml", ".md"}
65
+ OFFICE_EXTENSIONS = {".docx", ".pptx", ".xlsx"}
66
+ FONT_EXTENSIONS = {".ttf", ".otf"}
67
+ MODEL3D_EXTENSIONS = {".stl", ".obj", ".glb", ".gltf"}
68
+ ARCHIVE_EXTENSIONS = {".zip", ".tar", ".gz"}
69
+ BIM_EXTENSIONS = {".ifc"}
70
+
71
+
72
+
73
+ @router.post("/upload")
74
+ async def upload_asset(
75
+ file: UploadFile = File(...),
76
+ private: bool = Query(False),
77
+ folder: Optional[str] = Query(None),
78
+ current_user: Optional[str] = Depends(get_current_user)
79
+ ):
80
+ """
81
+ Upload a new asset.
82
+ - If 'private=True' and user is logged in: saved to 'users/{user_id}/{folder}/'
83
+ - Otherwise: saved to 'originals/{folder}/'
84
+ """
85
+ asset_uuid = str(uuid.uuid4())
86
+ ext = guess_extension(file.content_type) or ".bin"
87
+
88
+ # Determine base prefix
89
+ if private:
90
+ if not current_user:
91
+ raise HTTPException(status_code=401, detail="Authentication required for private uploads")
92
+ base_prefix = f"users/{current_user}"
93
+ else:
94
+ base_prefix = "originals"
95
+
96
+ # Sanitize and add custom folder if provided
97
+ path_parts = [base_prefix]
98
+ if folder:
99
+ sanitized_folder = folder.strip("/")
100
+ if sanitized_folder:
101
+ path_parts.append(sanitized_folder)
102
+
103
+ asset_id = f"{'/'.join(path_parts)}/{asset_uuid}{ext}"
104
+
105
+ try:
106
+ content = await file.read()
107
+ saved_id = await storage.save_asset(asset_id, content)
108
+
109
+ # Clean ID for the response (for private assets we keep the user prefix)
110
+ clean_id = saved_id if private else Path(saved_id).name
111
+
112
+ # Determine if it's a video to suggest thumbnail params
113
+ is_video = ext.lower() in VIDEO_EXTENSIONS
114
+
115
+ # Generate a sample signed URL
116
+ sample_fmt = ImageFormat.WEBP
117
+ sample_q = settings.default_quality
118
+ sig = generate_signature(
119
+ asset_id=clean_id,
120
+ width=None,
121
+ height=None,
122
+ format=sample_fmt.value.lower(),
123
+ quality=sample_q,
124
+ secret_key=settings.secret_key,
125
+ user_id=current_user if private else None
126
+ )
127
+
128
+ url = f"{settings.api_prefix}/assets/{clean_id}?s={sig}"
129
+ if is_video:
130
+ url += "&t=1"
131
+
132
+ return {
133
+ "asset_id": clean_id,
134
+ "url": url,
135
+ "is_private": private,
136
+ "owner": current_user if private else "public",
137
+ "mime_type": file.content_type,
138
+ "size": len(content)
139
+ }
140
+ except Exception as e:
141
+ raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
142
+
143
+
144
+ @router.get("/{asset_id:path}")
145
+ async def get_processed_asset(
146
+ asset_id: str,
147
+ w: Optional[int] = Query(None, alias="width", ge=1, le=settings.max_image_dimension),
148
+ h: Optional[int] = Query(None, alias="height", ge=1, le=settings.max_image_dimension),
149
+ fmt: Optional[ImageFormat] = Query(None, alias="format"),
150
+ q: Optional[int] = Query(None, alias="quality", ge=1, le=100),
151
+ preset: Optional[str] = Query(None, alias="preset"),
152
+ t: float = Query(0.0, alias="time", ge=0.0),
153
+ p: int = Query(1, alias="page", ge=1),
154
+ s: str = Query(..., alias="signature", description="HMAC-SHA256 signature"),
155
+ current_user: Optional[str] = Depends(get_current_user),
156
+ ):
157
+ """
158
+ Retrieve and process an asset.
159
+ Supports Smart Presets and User-bound protected assets.
160
+ """
161
+ # 0. Apply Preset Logic
162
+ target_w = w
163
+ target_h = h
164
+ target_fmt = fmt
165
+ target_q = q if q else settings.default_quality
166
+
167
+ if preset:
168
+ if preset not in settings.presets:
169
+ raise HTTPException(status_code=400, detail=f"Invalid preset: {preset}")
170
+
171
+ config = settings.presets[preset]
172
+ # Preset values act as defaults, explicit query params override them
173
+ target_w = w if w else config.get("width")
174
+ target_h = h if h else config.get("height")
175
+ target_fmt = fmt if fmt else ImageFormat(config.get("format").upper())
176
+ target_q = q if q else config.get("quality", settings.default_quality)
177
+
178
+ # 1. Signature Verification (SECURITY FIRST)
179
+ # We enforce that if an asset is in the "users/" directory, it MUST have a matching current_user
180
+ is_private = asset_id.startswith("users/")
181
+ if is_private:
182
+ # Extract owner ID from path: "users/{user_id}/file.jpg"
183
+ parts = asset_id.split("/")
184
+ if len(parts) < 3:
185
+ raise HTTPException(status_code=400, detail="Invalid private asset path")
186
+
187
+ owner_id = parts[1]
188
+ if current_user != owner_id:
189
+ raise HTTPException(status_code=403, detail="Not authorized to access this private asset")
190
+
191
+ is_valid = verify_signature(
192
+ asset_id=asset_id,
193
+ width=w,
194
+ height=h,
195
+ format=fmt.value.lower() if fmt else "",
196
+ quality=q if q else 0,
197
+ signature_to_verify=s,
198
+ secret_key=settings.secret_key,
199
+ preset=preset,
200
+ user_id=current_user
201
+ )
202
+
203
+ if not is_valid:
204
+ raise HTTPException(status_code=403, detail="Invalid signature")
205
+
206
+ options = ProcessingOptions(
207
+ width=target_w,
208
+ height=target_h,
209
+ format=fmt_val,
210
+ quality=target_q
211
+ )
212
+
213
+ # 2. Define Cache Paths (Include timestamp or page for media derivatives)
214
+ cache_key = options.get_cache_key()
215
+ is_video = Path(asset_id).suffix.lower() in VIDEO_EXTENSIONS
216
+ is_document = Path(asset_id).suffix.lower() in DOCUMENT_EXTENSIONS
217
+
218
+ if is_video:
219
+ cache_key = f"t{t}_{cache_key}"
220
+ elif is_document:
221
+ cache_key = f"p{p}_{cache_key}"
222
+
223
+ derivative_id = f"cache/{asset_id}/{cache_key}"
224
+
225
+ try:
226
+ # 3. Cache Check (HIT)
227
+ try:
228
+ derivative_bytes = await storage.get_asset(derivative_id)
229
+ return Response(
230
+ content=derivative_bytes,
231
+ media_type=f"image/{options.format.value.lower()}",
232
+ headers={
233
+ "Cache-Control": "public, max-age=31536000, immutable",
234
+ "X-MorphosX-Cache": "HIT"
235
+ }
236
+ )
237
+ except FileNotFoundError:
238
+ # 4. Cache Miss (MISS)
239
+ pass
240
+
241
+ # 5. Fetch Original (Always from originals/ folder)
242
+ original_id = f"originals/{asset_id}"
243
+ source_bytes = await storage.get_asset(original_id)
244
+
245
+ # 6. Transform Pipeline
246
+ is_audio = Path(asset_id).suffix.lower() in AUDIO_EXTENSIONS
247
+ is_raw = Path(asset_id).suffix.lower() in RAW_EXTENSIONS
248
+ is_text = Path(asset_id).suffix.lower() in TEXT_EXTENSIONS
249
+ is_office = Path(asset_id).suffix.lower() in OFFICE_EXTENSIONS
250
+ is_font = Path(asset_id).suffix.lower() in FONT_EXTENSIONS
251
+ is_model3d = Path(asset_id).suffix.lower() in MODEL3D_EXTENSIONS
252
+ is_archive = Path(asset_id).suffix.lower() in ARCHIVE_EXTENSIONS
253
+ is_bim = Path(asset_id).suffix.lower() in BIM_EXTENSIONS
254
+
255
+ if is_video:
256
+ # Video: Extract Frame -> Process as Image
257
+ frame_bytes = video_processor.extract_thumbnail(source_bytes, t)
258
+ processed_data, mime_type = processor.process(frame_bytes, options)
259
+ elif is_audio:
260
+ # Audio: Generate Waveform -> Process as Image
261
+ waveform_bytes = audio_processor.generate_waveform(source_bytes, w or 800, h or 200)
262
+ processed_data, mime_type = processor.process(waveform_bytes, options)
263
+ elif is_document:
264
+ # Document: Extract Page -> Process as Image
265
+ # We extract at 150 DPI to ensure crisp text before potential downscaling
266
+ page_bytes = document_processor.extract_page_as_image(source_bytes, p, dpi=150)
267
+ processed_data, mime_type = processor.process(page_bytes, options)
268
+ elif is_raw:
269
+ # RAW: Extract Preview -> Process as Image
270
+ preview_bytes = raw_processor.extract_preview(source_bytes)
271
+ processed_data, mime_type = processor.process(preview_bytes, options)
272
+ elif is_text:
273
+ # Text: Render to Image -> Process as Image
274
+ rendered_bytes = text_processor.render_to_image(source_bytes, asset_id, options)
275
+ processed_data, mime_type = processor.process(rendered_bytes, options)
276
+ elif is_office:
277
+ # Office: Generate Summary Card -> Process as Image
278
+ office_card_bytes = office_processor.render_thumbnail(source_bytes, asset_id)
279
+ processed_data, mime_type = processor.process(office_card_bytes, options)
280
+ elif is_font:
281
+ # Font: Render Specimen -> Process as Image
282
+ specimen_bytes = font_processor.render_specimen(source_bytes, {})
283
+ processed_data, mime_type = processor.process(specimen_bytes, options)
284
+ elif is_model3d:
285
+ # 3D: Generate Blueprint -> Process as Image
286
+ model_bytes = model3d_processor.render_thumbnail(source_bytes, asset_id)
287
+ processed_data, mime_type = processor.process(model_bytes, options)
288
+ elif is_archive:
289
+ # Archive: Generate Content List -> Process as Image
290
+ archive_bytes = archive_processor.render_thumbnail(source_bytes, asset_id)
291
+ processed_data, mime_type = processor.process(archive_bytes, options)
292
+ elif is_bim:
293
+ # BIM: Generate Building Data Card -> Process as Image
294
+ bim_bytes = bim_processor.render_summary(source_bytes, asset_id)
295
+ processed_data, mime_type = processor.process(bim_bytes, options)
296
+ else:
297
+ # Image: Process directly
298
+ processed_data, mime_type = processor.process(source_bytes, options)
299
+
300
+ # 7. Store derivative for future requests
301
+ await storage.save_asset(derivative_id, processed_data)
302
+
303
+ return Response(
304
+ content=processed_data,
305
+ media_type=mime_type,
306
+ headers={
307
+ "Cache-Control": "public, max-age=31536000, immutable",
308
+ "X-MorphosX-Cache": "MISS"
309
+ }
310
+ )
311
+
312
+ except FileNotFoundError:
313
+ raise HTTPException(status_code=404, detail="Asset not found")
314
+ except Exception as e:
315
+ raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
316
+
317
+
318
+ @router.get("/list/{path:path}")
319
+ async def list_assets(
320
+ path: str = "",
321
+ current_user: Optional[str] = Depends(get_current_user)
322
+ ):
323
+ """
324
+ List files and folders in a given path.
325
+ """
326
+ # Security: If path starts with users/, verify ownership
327
+ if path.startswith("users/"):
328
+ parts = path.split("/")
329
+ if len(parts) >= 2:
330
+ owner_id = parts[1]
331
+ if current_user != owner_id:
332
+ raise HTTPException(status_code=403, detail="Not authorized to browse this folder")
333
+
334
+ # If path is empty, default to listing 'originals/' (public root)
335
+ if not path:
336
+ path = "originals"
337
+
338
+ try:
339
+ items = await storage.list_assets(path)
340
+ return {
341
+ "path": path,
342
+ "items": items
343
+ }
344
+ except Exception as e:
345
+ raise HTTPException(status_code=500, detail=f"Listing failed: {str(e)}")
346
+