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 +21 -0
- morphosx-0.4.0/PKG-INFO +188 -0
- morphosx-0.4.0/README.md +124 -0
- morphosx-0.4.0/morphosx/app/__init__.py +0 -0
- morphosx-0.4.0/morphosx/app/api/__init__.py +0 -0
- morphosx-0.4.0/morphosx/app/api/assets.py +346 -0
- morphosx-0.4.0/morphosx/app/cli.py +22 -0
- morphosx-0.4.0/morphosx/app/core/__init__.py +0 -0
- morphosx-0.4.0/morphosx/app/core/auth.py +43 -0
- morphosx-0.4.0/morphosx/app/core/security.py +60 -0
- morphosx-0.4.0/morphosx/app/engine/__init__.py +0 -0
- morphosx-0.4.0/morphosx/app/engine/archive.py +58 -0
- morphosx-0.4.0/morphosx/app/engine/audio.py +40 -0
- morphosx-0.4.0/morphosx/app/engine/bim.py +91 -0
- morphosx-0.4.0/morphosx/app/engine/document.py +48 -0
- morphosx-0.4.0/morphosx/app/engine/font.py +55 -0
- morphosx-0.4.0/morphosx/app/engine/model3d.py +84 -0
- morphosx-0.4.0/morphosx/app/engine/office.py +78 -0
- morphosx-0.4.0/morphosx/app/engine/processor.py +140 -0
- morphosx-0.4.0/morphosx/app/engine/raw.py +44 -0
- morphosx-0.4.0/morphosx/app/engine/text.py +103 -0
- morphosx-0.4.0/morphosx/app/engine/video.py +78 -0
- morphosx-0.4.0/morphosx/app/engine/vips.py +96 -0
- morphosx-0.4.0/morphosx/app/main.py +28 -0
- morphosx-0.4.0/morphosx/app/settings.py +72 -0
- morphosx-0.4.0/morphosx/app/storage/__init__.py +0 -0
- morphosx-0.4.0/morphosx/app/storage/base.py +41 -0
- morphosx-0.4.0/morphosx/app/storage/local.py +84 -0
- morphosx-0.4.0/morphosx/app/storage/s3.py +106 -0
- morphosx-0.4.0/pyproject.toml +91 -0
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.
|
morphosx-0.4.0/PKG-INFO
ADDED
|
@@ -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
|
+
|
morphosx-0.4.0/README.md
ADDED
|
@@ -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
|
+
|