-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add a lot of fonts * update location of settings * fonts metadata api * something works * persist frame panel metadata in url * font selector * default font * move all fonts * move other assets * move folders * sync fonts * font app fonts * dedupe * bit better font select * template export select all * can toggle font * upload fonts * select font on scene * upload custom fonts * no init * font select * caret info * e2e with fonts * ubuntu font * cleanup * fix logic * pick nodes
- Loading branch information
1 parent
fd8c213
commit 14f615d
Showing
136 changed files
with
8,871 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import uuid | ||
from typing import Optional | ||
from http import HTTPStatus | ||
from fastapi import Depends, HTTPException, File, Form, UploadFile, Query | ||
from sqlalchemy.exc import SQLAlchemyError | ||
from sqlalchemy.orm import Session | ||
from fastapi.responses import Response | ||
|
||
from app.database import get_db | ||
from app.models.assets import Assets | ||
from app.schemas.assets import ( | ||
AssetResponse | ||
) | ||
from . import api_with_auth | ||
|
||
@api_with_auth.get("/assets", response_model=list[AssetResponse]) | ||
async def list_assets( | ||
path: Optional[str] = Query(None, description="Optional substring filter on the asset path"), | ||
db: Session = Depends(get_db) | ||
): | ||
""" | ||
Return a list of all stored Assets (without the binary data). | ||
Optionally filter by `path` if specified. | ||
""" | ||
query = db.query(Assets) | ||
if path: | ||
query = query.filter(Assets.path.ilike(f"%{path}%")) | ||
results = query.all() | ||
|
||
output = [] | ||
for asset in results: | ||
output.append(AssetResponse( | ||
id=asset.id, | ||
path=asset.path, | ||
size=len(asset.data) if asset.data else 0 | ||
)) | ||
return output | ||
|
||
|
||
@api_with_auth.get("/assets/{asset_id}", response_model=AssetResponse) | ||
async def get_asset(asset_id: str, db: Session = Depends(get_db)): | ||
""" | ||
Return metadata for a single asset by its ID. | ||
""" | ||
asset = db.query(Assets).filter_by(id=asset_id).first() | ||
if not asset: | ||
raise HTTPException(status_code=404, detail="Asset not found") | ||
|
||
return AssetResponse( | ||
id=asset.id, | ||
path=asset.path, | ||
size=len(asset.data) if asset.data else 0 | ||
) | ||
|
||
|
||
@api_with_auth.get("/assets/{asset_id}/download") | ||
async def download_asset(asset_id: str, db: Session = Depends(get_db)): | ||
""" | ||
Download the raw binary data of an asset by ID. | ||
""" | ||
asset = db.query(Assets).filter_by(id=asset_id).first() | ||
if not asset: | ||
raise HTTPException(status_code=404, detail="Asset not found") | ||
if not asset.data: | ||
raise HTTPException(status_code=404, detail="Asset has no data") | ||
|
||
return Response( | ||
content=asset.data, | ||
media_type="application/octet-stream", | ||
headers={"Content-Disposition": f'attachment; filename="{uuid.uuid4()}"'} | ||
) | ||
|
||
|
||
@api_with_auth.post("/assets", response_model=AssetResponse, status_code=201) | ||
async def create_asset( | ||
path: str = Form(..., description="Unique path identifier for this asset"), | ||
file: UploadFile = File(...), | ||
db: Session = Depends(get_db) | ||
): | ||
""" | ||
Create and store a new asset in the DB, reading from multipart/form-data. | ||
- `path` must be unique | ||
- `file` is the actual file data | ||
""" | ||
existing = db.query(Assets).filter_by(path=path).first() | ||
if existing: | ||
raise HTTPException( | ||
status_code=HTTPStatus.BAD_REQUEST, | ||
detail=f"Asset path '{path}' is already in use." | ||
) | ||
|
||
try: | ||
content = await file.read() | ||
except Exception: | ||
raise HTTPException(status_code=400, detail="Error reading uploaded file.") | ||
|
||
new_asset = Assets(path=path, data=content) | ||
db.add(new_asset) | ||
try: | ||
db.commit() | ||
except SQLAlchemyError: | ||
db.rollback() | ||
raise HTTPException(status_code=500, detail="Database error") | ||
|
||
return AssetResponse( | ||
id=new_asset.id, | ||
path=new_asset.path, | ||
size=len(new_asset.data) if new_asset.data else 0 | ||
) | ||
|
||
|
||
@api_with_auth.put("/assets/{asset_id}", response_model=AssetResponse) | ||
async def update_asset( | ||
asset_id: str, | ||
path: Optional[str] = Form(None, description="New path (must remain unique)"), | ||
file: Optional[UploadFile] = File(None), | ||
db: Session = Depends(get_db) | ||
): | ||
""" | ||
Update an existing asset with multipart/form-data. | ||
You can update: | ||
- The path (unique) | ||
- The file contents (if provided). | ||
If you only want to change the path (and not the file), omit `file`. | ||
""" | ||
asset = db.query(Assets).filter_by(id=asset_id).first() | ||
if not asset: | ||
raise HTTPException(status_code=404, detail="Asset not found") | ||
|
||
# If user wants to update the path: | ||
if path and path != asset.path: | ||
# check uniqueness of new path | ||
conflict = db.query(Assets).filter_by(path=path).first() | ||
if conflict and conflict.id != asset_id: | ||
raise HTTPException( | ||
status_code=HTTPStatus.BAD_REQUEST, | ||
detail=f"Another asset already uses path '{path}'." | ||
) | ||
asset.path = path | ||
|
||
# If user wants to update the binary data: | ||
if file is not None: | ||
try: | ||
content = await file.read() | ||
asset.data = content | ||
except Exception: | ||
raise HTTPException(status_code=400, detail="Error reading uploaded file.") | ||
|
||
try: | ||
db.commit() | ||
except SQLAlchemyError: | ||
db.rollback() | ||
raise HTTPException(status_code=500, detail="Database error") | ||
|
||
return AssetResponse( | ||
id=asset.id, | ||
path=asset.path, | ||
size=len(asset.data) if asset.data else 0 | ||
) | ||
|
||
|
||
@api_with_auth.delete("/assets/{asset_id}") | ||
async def delete_asset(asset_id: str, db: Session = Depends(get_db)): | ||
""" | ||
Delete an asset by ID. | ||
""" | ||
asset = db.query(Assets).filter_by(id=asset_id).first() | ||
if not asset: | ||
raise HTTPException(status_code=404, detail="Asset not found") | ||
|
||
db.delete(asset) | ||
try: | ||
db.commit() | ||
except SQLAlchemyError: | ||
db.rollback() | ||
raise HTTPException(status_code=500, detail="Database error") | ||
|
||
return {"message": "Asset deleted successfully"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import io | ||
import os | ||
from fastapi import Depends, HTTPException | ||
from fastapi.responses import FileResponse, StreamingResponse | ||
from sqlalchemy.orm import Session | ||
from app.database import get_db | ||
from app.models.assets import Assets | ||
from app.models.fonts import gather_all_fonts_info, parse_font_info_in_memory | ||
from app.schemas.fonts import FontMetadata, FontsListResponse | ||
from . import api_with_auth | ||
|
||
@api_with_auth.get("/fonts", response_model=FontsListResponse) | ||
async def api_fonts_list(db: Session = Depends(get_db)): | ||
""" | ||
Return a combined list of font metadata from: | ||
1) local ../frameos/assets/copied/fonts | ||
2) DB assets with path starting with fonts/ | ||
""" | ||
# 1) Gather local fonts from folder | ||
local_list = gather_all_fonts_info("../frameos/assets/copied/fonts") | ||
|
||
# 2) Gather DB fonts with path like "fonts/..." | ||
db_assets = db.query(Assets).filter(Assets.path.like("fonts/%")).all() # [NEW] | ||
for asset in db_assets: | ||
# asset.path is e.g. "fonts/MyFont.ttf" | ||
filename = os.path.basename(asset.path) | ||
if not filename.lower().endswith(".ttf"): | ||
continue | ||
# parse in-memory | ||
try: | ||
font_info = parse_font_info_in_memory(asset.data, filename) | ||
local_list.append(font_info.dict()) | ||
except Exception: | ||
# If we can't parse, skip or log | ||
pass | ||
|
||
# Build a combined list of FontMetadata objects | ||
combined_fonts = [] | ||
for item in local_list: | ||
# gather_all_fonts_info returns raw dicts, so unify them into FontMetadata | ||
if isinstance(item, dict): | ||
combined_fonts.append(FontMetadata(**item)) | ||
else: | ||
# or if gather_all_fonts_info returned FontMetadata directly | ||
combined_fonts.append(item) | ||
|
||
return {"fonts": combined_fonts} | ||
|
||
|
||
@api_with_auth.get("/fonts/{font_name}") | ||
async def api_fonts_download(font_name: str, db: Session = Depends(get_db)): | ||
""" | ||
Download a font by name. Checks DB first, then local folder. | ||
If found in DB, returns a StreamingResponse from memory. | ||
If found locally, returns a FileResponse from disk. | ||
""" | ||
# 1) Check DB for path="fonts/<font_name>" | ||
asset = db.query(Assets).filter_by(path=f"fonts/{font_name}").first() | ||
if asset: | ||
if not asset.data: | ||
raise HTTPException(status_code=404, detail="Font asset has no data") | ||
# return an in-memory streaming response | ||
return StreamingResponse( | ||
io.BytesIO(asset.data), | ||
media_type="font/ttf", | ||
headers={"Content-Disposition": f'attachment; filename="{font_name}"'}, | ||
) | ||
|
||
# 2) Check local folder | ||
local_path = f"../frameos/assets/copied/fonts/{font_name}" | ||
if "/" in font_name or "\\" in font_name: | ||
return {"error": "Invalid font filename"} | ||
if os.path.isfile(local_path): | ||
# Same logic as before. If you want a file download: | ||
return FileResponse( | ||
local_path, | ||
filename=font_name, | ||
media_type="font/ttf", | ||
headers={"Cache-Control": "max-age=86400"}, | ||
) | ||
|
||
return {"error": "font not found"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.