[feat] user assets (#3504)
This commit is contained in:
parent
c89721223f
commit
39c33699af
28 changed files with 1125 additions and 309 deletions
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter, FastAPI
|
||||||
from .db import core_app_extra, db
|
from .db import core_app_extra, db
|
||||||
from .views.admin_api import admin_router
|
from .views.admin_api import admin_router
|
||||||
from .views.api import api_router
|
from .views.api import api_router
|
||||||
|
from .views.asset_api import asset_router
|
||||||
from .views.audit_api import audit_router
|
from .views.audit_api import audit_router
|
||||||
from .views.auth_api import auth_router
|
from .views.auth_api import auth_router
|
||||||
from .views.callback_api import callback_router
|
from .views.callback_api import callback_router
|
||||||
|
|
@ -44,6 +45,7 @@ def init_core_routers(app: FastAPI):
|
||||||
app.include_router(webpush_router)
|
app.include_router(webpush_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
|
app.include_router(asset_router)
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
app.include_router(lnurl_router)
|
app.include_router(lnurl_router)
|
||||||
|
|
||||||
|
|
|
||||||
107
lnbits/core/crud/assets.py
Normal file
107
lnbits/core/crud/assets.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
from lnbits.core.db import db
|
||||||
|
from lnbits.core.models.assets import Asset, AssetFilters, AssetInfo
|
||||||
|
from lnbits.db import Connection, Filters, Page
|
||||||
|
|
||||||
|
|
||||||
|
async def create_asset(
|
||||||
|
entry: Asset,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).insert("assets", entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_asset_info(
|
||||||
|
user_id: str,
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_asset_info(
|
||||||
|
asset_id: str, conn: Connection | None = None
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_asset(
|
||||||
|
user_id: str,
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Asset | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
model=Asset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_public_asset(
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Asset | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=Asset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_public_asset_info(
|
||||||
|
asset_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> AssetInfo | None:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||||
|
values={"asset_id": asset_id},
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_asset_info(
|
||||||
|
asset: AssetInfo,
|
||||||
|
) -> AssetInfo:
|
||||||
|
await db.update("assets", asset)
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_user_asset(
|
||||||
|
user_id: str, asset_id: str, conn: Connection | None = None
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).execute(
|
||||||
|
query="DELETE FROM assets WHERE id = :asset_id AND user_id = :user_id",
|
||||||
|
values={"asset_id": asset_id, "user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_assets(
|
||||||
|
user_id: str,
|
||||||
|
filters: Filters[AssetFilters] | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Page[AssetInfo]:
|
||||||
|
filters = filters or Filters()
|
||||||
|
filters.sortby = filters.sortby or "created_at"
|
||||||
|
return await (conn or db).fetch_page(
|
||||||
|
query="SELECT * from assets",
|
||||||
|
where=["user_id = :user_id"],
|
||||||
|
values={"user_id": user_id},
|
||||||
|
filters=filters,
|
||||||
|
model=AssetInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_assets_count(user_id: str) -> int:
|
||||||
|
result = await db.execute(
|
||||||
|
query="SELECT COUNT(*) as count FROM assets WHERE user_id = :user_id",
|
||||||
|
values={"user_id": user_id},
|
||||||
|
)
|
||||||
|
row = result.mappings().first()
|
||||||
|
return row.get("count", 0)
|
||||||
|
|
@ -759,3 +759,22 @@ async def m036_add_shared_wallet_column(db: Connection):
|
||||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m037_create_assets_table(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
size_bytes INT NOT NULL,
|
||||||
|
thumbnail_base64 TEXT,
|
||||||
|
thumbnail {db.blob},
|
||||||
|
data {db.blob} NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
37
lnbits/core/models/assets.py
Normal file
37
lnbits/core/models/assets.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from lnbits.db import FilterModel
|
||||||
|
|
||||||
|
|
||||||
|
class AssetInfo(BaseModel):
|
||||||
|
id: str
|
||||||
|
mime_type: str
|
||||||
|
name: str
|
||||||
|
is_public: bool = False
|
||||||
|
size_bytes: int
|
||||||
|
thumbnail_base64: str | None = None
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(AssetInfo):
|
||||||
|
user_id: str
|
||||||
|
data: bytes
|
||||||
|
|
||||||
|
|
||||||
|
class AssetUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
is_public: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AssetFilters(FilterModel):
|
||||||
|
__search_fields__ = ["name"]
|
||||||
|
__sort_fields__ = [
|
||||||
|
"created_at",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
name: str | None = None
|
||||||
56
lnbits/core/services/assets.py
Normal file
56
lnbits/core/services/assets.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||||
|
from lnbits.core.models.assets import Asset
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) -> Asset:
|
||||||
|
if not file.content_type:
|
||||||
|
raise ValueError("File must have a content type.")
|
||||||
|
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
|
||||||
|
raise ValueError(f"File type '{file.content_type}' not allowed.")
|
||||||
|
|
||||||
|
if user_id not in settings.lnbits_assets_no_limit_users:
|
||||||
|
user_assets_count = await get_user_assets_count(user_id)
|
||||||
|
if user_assets_count >= settings.lnbits_max_assets_per_user:
|
||||||
|
raise ValueError(
|
||||||
|
f"Max upload count of {settings.lnbits_max_assets_per_user} exceeded."
|
||||||
|
)
|
||||||
|
|
||||||
|
contents = await file.read()
|
||||||
|
if len(contents) > settings.lnbits_max_asset_size_mb * 1024 * 1024:
|
||||||
|
raise ValueError(
|
||||||
|
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||||
|
)
|
||||||
|
|
||||||
|
image = Image.open(io.BytesIO(contents))
|
||||||
|
|
||||||
|
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
||||||
|
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
||||||
|
image.thumbnail((thumbnail_width, thumbnail_height))
|
||||||
|
|
||||||
|
# Save thumbnail to an in-memory buffer
|
||||||
|
thumb_buffer = io.BytesIO()
|
||||||
|
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
||||||
|
image.save(thumb_buffer, format=thumbnail_format)
|
||||||
|
thumb_buffer.seek(0)
|
||||||
|
|
||||||
|
asset = Asset(
|
||||||
|
id=uuid4().hex,
|
||||||
|
user_id=user_id,
|
||||||
|
mime_type=file.content_type,
|
||||||
|
is_public=is_public,
|
||||||
|
name=file.filename or "unnamed",
|
||||||
|
size_bytes=len(contents),
|
||||||
|
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
||||||
|
data=contents,
|
||||||
|
)
|
||||||
|
|
||||||
|
await create_asset(asset)
|
||||||
|
return asset
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from shutil import make_archive
|
||||||
from shutil import make_archive, move
|
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from typing import IO
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import filetype
|
from fastapi import APIRouter, Depends, File
|
||||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.core.models.misc import Image, SimpleStatus
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_admin_notification,
|
enqueue_admin_notification,
|
||||||
|
|
@ -23,7 +18,6 @@ from lnbits.core.services import (
|
||||||
from lnbits.core.services.notifications import send_email_notification
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.helpers import safe_upload_file_path
|
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
|
|
@ -172,93 +166,3 @@ async def api_download_backup() -> FileResponse:
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def upload_image(
|
|
||||||
file: UploadFile = file_upload,
|
|
||||||
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
|
||||||
) -> Image:
|
|
||||||
if not file.filename:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
|
||||||
)
|
|
||||||
|
|
||||||
# validate file types
|
|
||||||
file_info = filetype.guess(file.file)
|
|
||||||
if file_info is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
|
||||||
detail="Unable to determine file type",
|
|
||||||
)
|
|
||||||
detected_content_type = file_info.extension.lower()
|
|
||||||
if (
|
|
||||||
file.content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
or detected_content_type not in settings.lnbits_upload_allowed_types
|
|
||||||
):
|
|
||||||
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
|
||||||
|
|
||||||
# validate file name
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(file.filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{file.filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
# validate file size
|
|
||||||
real_file_size = 0
|
|
||||||
temp: IO = NamedTemporaryFile(delete=False)
|
|
||||||
for chunk in file.file:
|
|
||||||
real_file_size += len(chunk)
|
|
||||||
if real_file_size > content_length:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
|
||||||
detail=f"File too large ({content_length / 1000} KB max)",
|
|
||||||
)
|
|
||||||
temp.write(chunk)
|
|
||||||
temp.close()
|
|
||||||
|
|
||||||
move(temp.name, file_path)
|
|
||||||
return Image(filename=file.filename)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get(
|
|
||||||
"/api/v1/images",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def list_uploaded_images() -> list[Image]:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, "images")
|
|
||||||
files = image_folder.glob("*")
|
|
||||||
images = []
|
|
||||||
for file in files:
|
|
||||||
if file.is_file():
|
|
||||||
images.append(Image(filename=file.name))
|
|
||||||
return images
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete(
|
|
||||||
"/api/v1/images/{filename}",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
|
||||||
try:
|
|
||||||
file_path = safe_upload_file_path(filename)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail=f"The requested filename '{filename}' is forbidden.",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if not file_path.exists():
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
|
||||||
|
|
||||||
file_path.unlink()
|
|
||||||
return SimpleStatus(success=True, message=f"{filename} deleted")
|
|
||||||
|
|
|
||||||
174
lnbits/core/views/asset_api.py
Normal file
174
lnbits/core/views/asset_api.py
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import base64
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
||||||
|
|
||||||
|
from lnbits.core.crud.assets import (
|
||||||
|
delete_user_asset,
|
||||||
|
get_asset_info,
|
||||||
|
get_public_asset,
|
||||||
|
get_public_asset_info,
|
||||||
|
get_user_asset,
|
||||||
|
get_user_asset_info,
|
||||||
|
get_user_assets,
|
||||||
|
update_user_asset_info,
|
||||||
|
)
|
||||||
|
from lnbits.core.models.assets import AssetFilters, AssetInfo, AssetUpdate
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import User
|
||||||
|
from lnbits.core.services.assets import create_user_asset
|
||||||
|
from lnbits.db import Filters, Page
|
||||||
|
from lnbits.decorators import (
|
||||||
|
check_user_exists,
|
||||||
|
optional_user_id,
|
||||||
|
parse_filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_router = APIRouter(prefix="/api/v1/assets", tags=["Assets"])
|
||||||
|
|
||||||
|
upload_file_param = File(...)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/paginated",
|
||||||
|
name="Get user assets",
|
||||||
|
summary="Get paginated list user assets",
|
||||||
|
)
|
||||||
|
async def api_get_user_assets(
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
filters: Filters = Depends(parse_filters(AssetFilters)),
|
||||||
|
) -> Page[AssetInfo]:
|
||||||
|
return await get_user_assets(user.id, filters=filters)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Get user asset",
|
||||||
|
summary="Get user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset(
|
||||||
|
asset_id: str,
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
) -> AssetInfo:
|
||||||
|
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}/binary",
|
||||||
|
name="Get user asset binary",
|
||||||
|
summary="Get user asset binary data by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset_binary(
|
||||||
|
asset_id: str,
|
||||||
|
user_id: str | None = Depends(optional_user_id),
|
||||||
|
) -> Response:
|
||||||
|
asset = None
|
||||||
|
if user_id:
|
||||||
|
asset = await get_user_asset(user_id, asset_id)
|
||||||
|
|
||||||
|
if not asset:
|
||||||
|
asset = await get_public_asset(asset_id)
|
||||||
|
|
||||||
|
if not asset:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=asset.data,
|
||||||
|
media_type=asset.mime_type,
|
||||||
|
headers={"Content-Disposition": f'inline; filename="{asset.name}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.get(
|
||||||
|
"/{asset_id}/thumbnail",
|
||||||
|
name="Get user asset thumbnail",
|
||||||
|
summary="Get user asset thumbnail data by ID",
|
||||||
|
)
|
||||||
|
async def api_get_asset_thumbnail(
|
||||||
|
asset_id: str,
|
||||||
|
user_id: str | None = Depends(optional_user_id),
|
||||||
|
) -> Response:
|
||||||
|
asset_info = None
|
||||||
|
if user_id:
|
||||||
|
asset_info = await get_user_asset_info(user_id, asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
asset_info = await get_public_asset_info(asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=(
|
||||||
|
base64.b64decode(asset_info.thumbnail_base64)
|
||||||
|
if asset_info.thumbnail_base64
|
||||||
|
else b""
|
||||||
|
),
|
||||||
|
media_type=asset_info.mime_type,
|
||||||
|
headers={"Content-Disposition": f'inline; filename="{asset_info.name}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.put(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Update user asset",
|
||||||
|
summary="Update user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_update_asset(
|
||||||
|
asset_id: str,
|
||||||
|
data: AssetUpdate,
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
) -> AssetInfo:
|
||||||
|
if user.admin:
|
||||||
|
asset_info = await get_asset_info(asset_id)
|
||||||
|
else:
|
||||||
|
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||||
|
|
||||||
|
if not asset_info:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
asset_info.name = data.name or asset_info.name
|
||||||
|
asset_info.is_public = (
|
||||||
|
asset_info.is_public if data.is_public is None else data.is_public
|
||||||
|
)
|
||||||
|
await update_user_asset_info(asset_info)
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.post(
|
||||||
|
"",
|
||||||
|
name="Upload",
|
||||||
|
summary="Upload user assets",
|
||||||
|
)
|
||||||
|
async def api_upload_asset(
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
file: UploadFile = upload_file_param,
|
||||||
|
public_asset: bool = False,
|
||||||
|
) -> AssetInfo:
|
||||||
|
asset = await create_user_asset(user.id, file, public_asset)
|
||||||
|
|
||||||
|
asset_info = await get_user_asset_info(user.id, asset.id)
|
||||||
|
if not asset_info:
|
||||||
|
raise ValueError("Failed to retrieve asset info after upload.")
|
||||||
|
|
||||||
|
return asset_info
|
||||||
|
|
||||||
|
|
||||||
|
@asset_router.delete(
|
||||||
|
"/{asset_id}",
|
||||||
|
name="Delete user asset",
|
||||||
|
summary="Delete user asset by ID",
|
||||||
|
)
|
||||||
|
async def api_delete_asset(
|
||||||
|
asset_id: str,
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
) -> SimpleStatus:
|
||||||
|
asset = await get_user_asset(user.id, asset_id)
|
||||||
|
if not asset:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||||
|
|
||||||
|
await delete_user_asset(user.id, asset_id)
|
||||||
|
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
||||||
|
|
@ -130,6 +130,12 @@ class Compat:
|
||||||
return "BIGINT"
|
return "BIGINT"
|
||||||
return "INT"
|
return "INT"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blob(self) -> str:
|
||||||
|
if self.type in {POSTGRES}:
|
||||||
|
return "BYTEA"
|
||||||
|
return "BLOB"
|
||||||
|
|
||||||
def timestamp_placeholder(self, key: str) -> str:
|
def timestamp_placeholder(self, key: str) -> str:
|
||||||
return compat_timestamp_placeholder(key)
|
return compat_timestamp_placeholder(key)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,17 +356,6 @@ def normalize_path(path: str | None) -> str:
|
||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
|
||||||
image_folder = Path(settings.lnbits_data_folder, directory)
|
|
||||||
file_path = image_folder / filename
|
|
||||||
# Prevent dir traversal attack
|
|
||||||
if image_folder.resolve() not in file_path.resolve().parents:
|
|
||||||
raise ValueError("Unsafe filename.")
|
|
||||||
# Prevent filename with subdirectories
|
|
||||||
file_path = image_folder / filename.split("/")[-1]
|
|
||||||
return file_path.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
if add_proto:
|
if add_proto:
|
||||||
|
|
|
||||||
|
|
@ -280,8 +280,11 @@ class ThemesSettings(LNbitsSettings):
|
||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
|
|
||||||
lnbits_upload_allowed_types: list[str] = Field(
|
|
||||||
|
class AssetSettings(LNbitsSettings):
|
||||||
|
lnbits_max_asset_size_mb: float = Field(default=2.5, ge=0.0)
|
||||||
|
lnbits_assets_allowed_mime_types: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
|
|
@ -297,6 +300,12 @@ class OpsSettings(LNbitsSettings):
|
||||||
"heics",
|
"heics",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||||
|
lnbits_asset_thumbnail_height: int = Field(default=128, ge=0)
|
||||||
|
lnbits_asset_thumbnail_format: str = Field(default="png")
|
||||||
|
|
||||||
|
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
|
||||||
|
lnbits_assets_no_limit_users: list[str] = Field(default=[])
|
||||||
|
|
||||||
|
|
||||||
class FeeSettings(LNbitsSettings):
|
class FeeSettings(LNbitsSettings):
|
||||||
|
|
@ -867,6 +876,7 @@ class EditableSettings(
|
||||||
ExtensionsSettings,
|
ExtensionsSettings,
|
||||||
ThemesSettings,
|
ThemesSettings,
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
|
AssetSettings,
|
||||||
FeeSettings,
|
FeeSettings,
|
||||||
ExchangeProvidersSettings,
|
ExchangeProvidersSettings,
|
||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
|
|
|
||||||
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -149,6 +149,8 @@ window.localisation.en = {
|
||||||
extensions: 'Extensions',
|
extensions: 'Extensions',
|
||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
|
created_at: 'Created At',
|
||||||
|
updated_at: 'Updated At',
|
||||||
search_extensions: 'Search extensions',
|
search_extensions: 'Search extensions',
|
||||||
search_wallets: 'Search wallets',
|
search_wallets: 'Search wallets',
|
||||||
extension_sources: 'Extension Sources',
|
extension_sources: 'Extension Sources',
|
||||||
|
|
@ -159,6 +161,7 @@ window.localisation.en = {
|
||||||
repository: 'Repository',
|
repository: 'Repository',
|
||||||
confirm_continue: 'Are you sure you want to continue?',
|
confirm_continue: 'Are you sure you want to continue?',
|
||||||
manage_extension_details: 'Install/uninstall extension',
|
manage_extension_details: 'Install/uninstall extension',
|
||||||
|
upload: 'Upload',
|
||||||
install: 'Install',
|
install: 'Install',
|
||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
|
|
@ -407,6 +410,8 @@ window.localisation.en = {
|
||||||
first_name: 'First Name',
|
first_name: 'First Name',
|
||||||
last_name: 'Last Name',
|
last_name: 'Last Name',
|
||||||
picture: 'Picture',
|
picture: 'Picture',
|
||||||
|
user_picture_desc:
|
||||||
|
'URL to an image to use as profile picture. You can upload it as an asset.',
|
||||||
verify_email: 'Verify email with',
|
verify_email: 'Verify email with',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
update_account: 'Update Account',
|
update_account: 'Update Account',
|
||||||
|
|
@ -431,6 +436,26 @@ window.localisation.en = {
|
||||||
toggle_gradient: 'Toggle Gradient',
|
toggle_gradient: 'Toggle Gradient',
|
||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
|
assets: 'Assets',
|
||||||
|
max_asset_size_mb: 'Max Asset Size (MB)',
|
||||||
|
max_asset_size_mb_desc:
|
||||||
|
'The maximum allowed size for asset uploads in megabytes (can use decimal values).',
|
||||||
|
assets_allowed_mime_types: 'Allowed MIME Types',
|
||||||
|
assets_allowed_mime_types_desc:
|
||||||
|
'The MIME types that are allowed for asset uploads. No value means all uploads are allowed.',
|
||||||
|
thumbnail_width: 'Thumbnail Width',
|
||||||
|
thumbnail_width_desc: 'Width of the generated thumbnail in pixels.',
|
||||||
|
thumbnail_height: 'Thumbnail Height',
|
||||||
|
thumbnail_height_desc: 'Height of the generated thumbnail in pixels.',
|
||||||
|
thumbnail_format: 'Thumbnail Format',
|
||||||
|
thumbnail_format_desc:
|
||||||
|
'Image format of the generated thumbnail (PNG, JPEG, etc.).',
|
||||||
|
max_assets_per_user: 'Max Assets Per User',
|
||||||
|
max_assets_per_user_desc:
|
||||||
|
'The maximum number of assets a user can upload. Zero means upload forbidden.',
|
||||||
|
assets_no_limit_users: 'Users Without Asset Limits',
|
||||||
|
assets_no_limit_users_desc:
|
||||||
|
'These users can upload an unlimited number of assets (user id based).',
|
||||||
color_scheme: 'Color Scheme',
|
color_scheme: 'Color Scheme',
|
||||||
visible_wallet_count: 'Visible Wallet Count',
|
visible_wallet_count: 'Visible Wallet Count',
|
||||||
admin_settings: 'Admin Settings',
|
admin_settings: 'Admin Settings',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
window.app.component('lnbits-admin-assets-config', {
|
||||||
|
props: ['form-data'],
|
||||||
|
template: '#lnbits-admin-assets-config',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newAllowedAssetMimeType: '',
|
||||||
|
newNoLimitUser: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {},
|
||||||
|
methods: {
|
||||||
|
addAllowedAssetMimeType() {
|
||||||
|
if (this.newAllowedAssetMimeType) {
|
||||||
|
this.removeAllowedAssetMimeType(this.newAllowedAssetMimeType)
|
||||||
|
this.formData.lnbits_assets_allowed_mime_types.push(
|
||||||
|
this.newAllowedAssetMimeType
|
||||||
|
)
|
||||||
|
this.newAllowedAssetMimeType = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllowedAssetMimeType(type) {
|
||||||
|
const index = this.formData.lnbits_assets_allowed_mime_types.indexOf(type)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNewNoLimitUser() {
|
||||||
|
if (this.newNoLimitUser) {
|
||||||
|
this.removeNoLimitUser(this.newNoLimitUser)
|
||||||
|
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
||||||
|
this.newNoLimitUser = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeNoLimitUser(user) {
|
||||||
|
if (user) {
|
||||||
|
this.formData.lnbits_assets_no_limit_users =
|
||||||
|
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
window.app.component('lnbits-admin-library', {
|
|
||||||
props: ['form-data'],
|
|
||||||
template: '#lnbits-admin-library',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
library_images: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
await this.getUploadedImages()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onImageInput(e) {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
this.uploadImage(file)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadImage(file) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'POST',
|
|
||||||
'/admin/api/v1/images',
|
|
||||||
this.g.user.wallets[0].adminkey,
|
|
||||||
formData,
|
|
||||||
{headers: {'Content-Type': 'multipart/form-data'}}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Image uploaded!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.getUploadedImages()
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
getUploadedImages() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
|
|
||||||
.then(response => {
|
|
||||||
this.library_images = response.data.map(image => ({
|
|
||||||
...image,
|
|
||||||
url: `${window.origin}/${image.directory}/${image.filename}`
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
deleteImage(filename) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this image?')
|
|
||||||
.onOk(() => {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
`/admin/api/v1/images/${filename}`,
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Image deleted!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
this.getUploadedImages()
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -82,6 +82,31 @@ window.PageAccount = {
|
||||||
allRead: false,
|
allRead: false,
|
||||||
allWrite: false
|
allWrite: false
|
||||||
},
|
},
|
||||||
|
assets: [],
|
||||||
|
assetsTable: {
|
||||||
|
loading: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('Name'),
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'created_at',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('created_at'),
|
||||||
|
field: 'created_at',
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 6,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assetsUploadToPublic: false,
|
||||||
notifications: {
|
notifications: {
|
||||||
nostr: {
|
nostr: {
|
||||||
identifier: ''
|
identifier: ''
|
||||||
|
|
@ -89,6 +114,17 @@ window.PageAccount = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'assetsTable.search': {
|
||||||
|
handler() {
|
||||||
|
const props = {}
|
||||||
|
if (this.assetsTable.search) {
|
||||||
|
props['search'] = this.assetsTable.search
|
||||||
|
}
|
||||||
|
this.getUserAssets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
|
|
@ -400,6 +436,94 @@ window.PageAccount = {
|
||||||
} finally {
|
} finally {
|
||||||
this.apiAcl.password = ''
|
this.apiAcl.password = ''
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async getUserAssets(props) {
|
||||||
|
try {
|
||||||
|
this.assetsTable.loading = true
|
||||||
|
const params = LNbits.utils.prepareFilterQuery(this.assetsTable, props)
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/assets/paginated?${params}`,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
this.assets = data.data
|
||||||
|
this.assetsTable.pagination.rowsNumber = data.total
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
} finally {
|
||||||
|
this.assetsTable.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onImageInput(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
this.uploadAsset(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uploadAsset(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
|
||||||
|
null,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {'Content-Type': 'multipart/form-data'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Upload successful!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
await this.getUserAssets()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteAsset(asset) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this asset?')
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/assets/${asset.id}`,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Asset deleted.'
|
||||||
|
})
|
||||||
|
await this.getUserAssets()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async toggleAssetPublicAccess(asset) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request('PUT', `/api/v1/assets/${asset.id}`, null, {
|
||||||
|
is_public: !asset.is_public
|
||||||
|
})
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Update successful!',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
await this.getUserAssets()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyAssetLinkToClipboard(asset) {
|
||||||
|
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
||||||
|
this.copyText(assetUrl)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -417,5 +541,6 @@ window.PageAccount = {
|
||||||
this.tab = hash
|
this.tab = hash
|
||||||
}
|
}
|
||||||
await this.getApiACLs()
|
await this.getApiACLs()
|
||||||
|
await this.getUserAssets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
"js/components/admin/lnbits-admin-extensions.js",
|
"js/components/admin/lnbits-admin-extensions.js",
|
||||||
"js/components/admin/lnbits-admin-notifications.js",
|
"js/components/admin/lnbits-admin-notifications.js",
|
||||||
"js/components/admin/lnbits-admin-site-customisation.js",
|
"js/components/admin/lnbits-admin-site-customisation.js",
|
||||||
"js/components/admin/lnbits-admin-library.js",
|
"js/components/admin/lnbits-admin-assets-config.js",
|
||||||
"js/components/admin/lnbits-admin-audit.js",
|
"js/components/admin/lnbits-admin-audit.js",
|
||||||
"js/components/lnbits-home-logos.js",
|
"js/components/lnbits-home-logos.js",
|
||||||
"js/components/lnbits-new-user-wallet.js",
|
"js/components/lnbits-new-user-wallet.js",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ include('components/admin/users.vue') %} {%
|
||||||
include('components/admin/site_customisation.vue') %} {%
|
include('components/admin/site_customisation.vue') %} {%
|
||||||
include('components/admin/audit.vue') %} {%
|
include('components/admin/audit.vue') %} {%
|
||||||
include('components/admin/extensions.vue') %} {%
|
include('components/admin/extensions.vue') %} {%
|
||||||
include('components/admin/library.vue') %} {%
|
include('components/admin/assets-config.vue') %} {%
|
||||||
include('components/admin/notifications.vue') %} {%
|
include('components/admin/notifications.vue') %} {%
|
||||||
include('components/admin/server.vue') %} {%
|
include('components/admin/server.vue') %} {%
|
||||||
include('components/new_user_wallet.vue') %} {%
|
include('components/new_user_wallet.vue') %} {%
|
||||||
|
|
|
||||||
154
lnbits/templates/components/admin/assets-config.vue
Normal file
154
lnbits/templates/components/admin/assets-config.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template id="lnbits-admin-assets-config">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<h6 class="q-my-none q-mb-sm">Assets</h6>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('max_asset_size_mb')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_max_asset_size_mb"
|
||||||
|
:label="$t('max_asset_size_mb')"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
:hint="$t('max_asset_size_mb_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-9">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('assets_allowed_mime_types')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model.number="newAllowedAssetMimeType"
|
||||||
|
@keydown.enter="addAllowedAssetMimeType()"
|
||||||
|
:label="$t('assets_allowed_mime_types')"
|
||||||
|
:hint="$t('assets_allowed_mime_types_desc')"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="addAllowedAssetMimeType()"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="add"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<div>
|
||||||
|
<q-chip
|
||||||
|
v-for="type in formData.lnbits_assets_allowed_mime_types"
|
||||||
|
:key="type"
|
||||||
|
removable
|
||||||
|
@remove="removeAllowedAssetMimeType(type)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
class="ellipsis"
|
||||||
|
:label="type"
|
||||||
|
><q-tooltip
|
||||||
|
v-if="identifier"
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
><span v-text="identifier"></span></q-tooltip
|
||||||
|
></q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||||
|
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('thumbnail_width')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_asset_thumbnail_width"
|
||||||
|
:label="$t('thumbnail_width')"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
:hint="$t('thumbnail_width_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('thumbnail_height')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_asset_thumbnail_height"
|
||||||
|
:label="$t('thumbnail_height')"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
:hint="$t('thumbnail_height_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('thumbnail_format')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model.number="formData.lnbits_asset_thumbnail_format"
|
||||||
|
:label="$t('thumbnail_format')"
|
||||||
|
:hint="$t('thumbnail_format_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||||
|
<h6 class="q-my-none q-mb-sm">Users</h6>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('max_assets_per_user')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_max_assets_per_user"
|
||||||
|
:label="$t('max_assets_per_user')"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
:hint="$t('max_assets_per_user_desc')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-9">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('assets_no_limit_users')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model.number="newNoLimitUser"
|
||||||
|
@keydown.enter="addNewNoLimitUser()"
|
||||||
|
:label="$t('assets_no_limit_users')"
|
||||||
|
:hint="$t('assets_no_limit_users_desc')"
|
||||||
|
>
|
||||||
|
<q-btn @click="addNewNoLimitUser()" dense flat icon="add"></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<div>
|
||||||
|
<q-chip
|
||||||
|
v-for="type in formData.lnbits_assets_no_limit_users"
|
||||||
|
:key="type"
|
||||||
|
removable
|
||||||
|
@remove="removeNoLimitUser(type)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
class="ellipsis"
|
||||||
|
:label="type"
|
||||||
|
><q-tooltip
|
||||||
|
v-if="identifier"
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
><span v-text="identifier"></span></q-tooltip
|
||||||
|
></q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<!-- for spacing -->
|
||||||
|
<br />
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
<template id="lnbits-admin-library">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<h6 class="q-my-none q-mb-sm">
|
|
||||||
<span v-text="$t('image_library')"></span>
|
|
||||||
</h6>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
label="Add Image"
|
|
||||||
@click="$refs.imageInput.click()"
|
|
||||||
class="q-mb-md"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref="imageInput"
|
|
||||||
accept="image/png, image/jpeg, image/gif"
|
|
||||||
style="display: none"
|
|
||||||
@change="onImageInput"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<div class="row q-col-gutter-sm q-pa-sm">
|
|
||||||
<div
|
|
||||||
v-for="image in library_images"
|
|
||||||
:key="image.filename"
|
|
||||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
|
||||||
style="max-width: 200px"
|
|
||||||
>
|
|
||||||
<q-card class="q-mb-sm">
|
|
||||||
<q-img :src="image.url" style="height: 150px" />
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="q-pt-md q-pb-md row items-center justify-between"
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
><div
|
|
||||||
class="text-caption ellipsis"
|
|
||||||
style="max-width: 100px"
|
|
||||||
:title="image.filename"
|
|
||||||
v-text="image.filename"
|
|
||||||
></div
|
|
||||||
></small>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="content_copy"
|
|
||||||
@click="copyText(image.url)"
|
|
||||||
:title="$t('copy')"
|
|
||||||
><q-tooltip>Copy image link</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
size="sm"
|
|
||||||
icon="delete"
|
|
||||||
color="negative"
|
|
||||||
@click="deleteImage(image.filename)"
|
|
||||||
:title="$t('delete')"
|
|
||||||
><q-tooltip>Delete image</q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="library_images.length === 0" class="q-pa-xl">
|
|
||||||
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -62,6 +62,16 @@
|
||||||
><span v-text="$t('access_control_list')"></span
|
><span v-text="$t('access_control_list')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-tab>
|
</q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="assets"
|
||||||
|
icon="perm_media"
|
||||||
|
:label="$q.screen.gt.sm ? $t('assets') : ''"
|
||||||
|
@update="val => (tab = val.name)"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('assets')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-tab>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:after>
|
<template v-slot:after>
|
||||||
|
|
@ -322,6 +332,7 @@
|
||||||
<q-input
|
<q-input
|
||||||
v-model="user.extra.picture"
|
v-model="user.extra.picture"
|
||||||
:label="$t('picture')"
|
:label="$t('picture')"
|
||||||
|
:hint="$t('user_picture_desc')"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
|
|
@ -951,6 +962,178 @@
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="assets">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-2 col-sm-12">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
:label="$t('upload')"
|
||||||
|
@click="$refs.imageInput.click()"
|
||||||
|
class="full-width"
|
||||||
|
></q-btn>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="imageInput"
|
||||||
|
style="display: none"
|
||||||
|
@change="onImageInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-toggle
|
||||||
|
v-model="assetsUploadToPublic"
|
||||||
|
label="Visible for everyone (public)"
|
||||||
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
:label="$t('search')"
|
||||||
|
dense
|
||||||
|
class="full-width q-pb-xl"
|
||||||
|
v-model="assetsTable.search"
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search"> </q-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
v-if="assetsTable.search !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="assetsTable.search = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-card-section>
|
||||||
|
<q-table
|
||||||
|
grid
|
||||||
|
grid-header
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
:rows="assets"
|
||||||
|
:columns="assetsTable.columns"
|
||||||
|
v-model:pagination="assetsTable.pagination"
|
||||||
|
:loading="assetsTable.loading"
|
||||||
|
@request="getUserAssets"
|
||||||
|
row-key="id"
|
||||||
|
:filter="filter"
|
||||||
|
hide-header
|
||||||
|
>
|
||||||
|
<template v-slot:item="props">
|
||||||
|
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
||||||
|
<q-card class="q-ma-sm wallet-list-card text-center">
|
||||||
|
<q-card-section>
|
||||||
|
<a
|
||||||
|
v-if="props.row.thumbnail_base64"
|
||||||
|
target="_blank"
|
||||||
|
style="color: inherit"
|
||||||
|
:href="`/api/v1/assets/${props.row.id}/binary`"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
:src="
|
||||||
|
'data:image/png;base64,' +
|
||||||
|
props.row.thumbnail_base64
|
||||||
|
"
|
||||||
|
:alt="props.row.name"
|
||||||
|
loading="lazy"
|
||||||
|
style="height: 128px"
|
||||||
|
class="text-center cursor-pointer"
|
||||||
|
>
|
||||||
|
</q-img>
|
||||||
|
</a>
|
||||||
|
<q-icon v-else name="web_asset"></q-icon>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn-dropdown
|
||||||
|
color="grey"
|
||||||
|
dense
|
||||||
|
outline
|
||||||
|
no-caps
|
||||||
|
:label="props.row.name"
|
||||||
|
:icon="props.row.is_public ? 'public' : ''"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="copyAssetLinkToClipboard(props.row)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="content_copy" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Copy Link</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Copy asset link to
|
||||||
|
clipboard</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="toggleAssetPublicAccess(props.row)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar
|
||||||
|
:icon="
|
||||||
|
props.row.is_public
|
||||||
|
? 'public_off'
|
||||||
|
: 'public'
|
||||||
|
"
|
||||||
|
text-color="primary"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section v-if="props.row.is_public">
|
||||||
|
<q-item-label>Unpublish</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Make this asset private</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section v-else>
|
||||||
|
<q-item-label>Publish</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Make this asset public</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="deleteAsset(props.row)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar
|
||||||
|
icon="delete"
|
||||||
|
text-color="negative"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Delete</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Permanently delete this
|
||||||
|
asset</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
{value: 'extensions', label: $t('extensions')},
|
{value: 'extensions', label: $t('extensions')},
|
||||||
{value: 'notifications', label: $t('notifications')},
|
{value: 'notifications', label: $t('notifications')},
|
||||||
{value: 'audit', label: $t('audit')},
|
{value: 'audit', label: $t('audit')},
|
||||||
{value: 'library', label: $t('Library')},
|
{value: 'assets-config', label: $t('assets')},
|
||||||
{value: 'site_customisation', label: $t('site_customisation')}
|
{value: 'site_customisation', label: $t('site_customisation')}
|
||||||
]"
|
]"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
|
|
@ -176,12 +176,12 @@
|
||||||
><span v-text="$t('audit')"></span></q-tooltip
|
><span v-text="$t('audit')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
<q-tab
|
<q-tab
|
||||||
name="library"
|
name="assets-config"
|
||||||
icon="image"
|
icon="perm_media"
|
||||||
:label="$q.screen.gt.sm ? $t('library') : null"
|
:label="$q.screen.gt.sm ? $t('assets') : null"
|
||||||
@update="val => (tab = val.name)"
|
@update="val => (tab = val.name)"
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('library')"></span></q-tooltip
|
><span v-text="$t('assets')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
<q-tab
|
<q-tab
|
||||||
style="word-break: break-all"
|
style="word-break: break-all"
|
||||||
|
|
@ -240,8 +240,8 @@
|
||||||
<q-tab-panel name="audit">
|
<q-tab-panel name="audit">
|
||||||
<lnbits-admin-audit :form-data="formData" />
|
<lnbits-admin-audit :form-data="formData" />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="library">
|
<q-tab-panel name="assets-config">
|
||||||
<lnbits-admin-library :form-data="formData" />
|
<lnbits-admin-assets-config :form-data="formData" />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="copyText(auditDetailsDialog.data)"
|
@click="copyText(auditDetailsDialog.data)"
|
||||||
icon="copy_content"
|
icon="content_copy"
|
||||||
color="grey"
|
color="grey"
|
||||||
flat
|
flat
|
||||||
v-text="$t('copy')"
|
v-text="$t('copy')"
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
"js/components/admin/lnbits-admin-extensions.js",
|
"js/components/admin/lnbits-admin-extensions.js",
|
||||||
"js/components/admin/lnbits-admin-notifications.js",
|
"js/components/admin/lnbits-admin-notifications.js",
|
||||||
"js/components/admin/lnbits-admin-site-customisation.js",
|
"js/components/admin/lnbits-admin-site-customisation.js",
|
||||||
"js/components/admin/lnbits-admin-library.js",
|
"js/components/admin/lnbits-admin-assets-config.js",
|
||||||
"js/components/admin/lnbits-admin-audit.js",
|
"js/components/admin/lnbits-admin-audit.js",
|
||||||
"js/components/lnbits-home-logos.js",
|
"js/components/lnbits-home-logos.js",
|
||||||
"js/components/lnbits-new-user-wallet.js",
|
"js/components/lnbits-new-user-wallet.js",
|
||||||
|
|
|
||||||
113
poetry.lock
generated
113
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
|
|
@ -2664,6 +2664,115 @@ files = [
|
||||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.0.0"
|
||||||
|
description = "Python Imaging Library (fork)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"},
|
||||||
|
{file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"},
|
||||||
|
{file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"},
|
||||||
|
{file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"},
|
||||||
|
{file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"},
|
||||||
|
{file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"},
|
||||||
|
{file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"},
|
||||||
|
{file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||||
|
fpx = ["olefile"]
|
||||||
|
mic = ["olefile"]
|
||||||
|
test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
|
||||||
|
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
|
||||||
|
xmp = ["defusedxml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.3.8"
|
version = "4.3.8"
|
||||||
|
|
@ -4592,4 +4701,4 @@ migration = ["psycopg2-binary"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "6b3b2f3c3163bc7a7bc2029ff6dd67f67c37057d9efbdf866b9d744f9e4ee9a5"
|
content-hash = "4b7aa07c5bc791f2d25da796c7ca242e543e0216b8992e27da218f8aef9b3120"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ dependencies = [
|
||||||
"nostr-sdk==0.42.1",
|
"nostr-sdk==0.42.1",
|
||||||
"bcrypt==4.3.0",
|
"bcrypt==4.3.0",
|
||||||
"jsonpath-ng==1.7.0",
|
"jsonpath-ng==1.7.0",
|
||||||
|
"pillow>=12.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from lnbits.helpers import safe_upload_file_path
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"filepath",
|
|
||||||
[
|
|
||||||
"test.txt",
|
|
||||||
"test/test.txt",
|
|
||||||
"test/test/test.txt",
|
|
||||||
"test/../test.txt",
|
|
||||||
"*/test.txt",
|
|
||||||
"test/**/test.txt",
|
|
||||||
"./test.txt",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_safe_upload_file_path(filepath: str):
|
|
||||||
safe_path = safe_upload_file_path(filepath)
|
|
||||||
assert safe_path.name == "test.txt"
|
|
||||||
|
|
||||||
# check if subdirectories got removed
|
|
||||||
images_folder = Path(settings.lnbits_data_folder) / "images"
|
|
||||||
assert images_folder.resolve() / "test.txt" == safe_path
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"filepath",
|
|
||||||
[
|
|
||||||
"../test.txt",
|
|
||||||
"test/../../test.txt",
|
|
||||||
"../../test.txt",
|
|
||||||
"test/../../../test.txt",
|
|
||||||
"../../../test.txt",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_unsafe_upload_file_path(filepath: str):
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
safe_upload_file_path(filepath)
|
|
||||||
56
uv.lock
generated
56
uv.lock
generated
|
|
@ -938,6 +938,8 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
|
||||||
|
|
@ -947,6 +949,8 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
||||||
|
|
@ -956,6 +960,8 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1313,6 +1319,7 @@ dependencies = [
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "nostr-sdk" },
|
{ name = "nostr-sdk" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
|
{ name = "pillow" },
|
||||||
{ name = "protobuf" },
|
{ name = "protobuf" },
|
||||||
{ name = "pycryptodomex" },
|
{ name = "pycryptodomex" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
|
@ -1396,6 +1403,7 @@ requires-dist = [
|
||||||
{ name = "loguru", specifier = "==0.7.3" },
|
{ name = "loguru", specifier = "==0.7.3" },
|
||||||
{ name = "nostr-sdk", specifier = "==0.42.1" },
|
{ name = "nostr-sdk", specifier = "==0.42.1" },
|
||||||
{ name = "packaging", specifier = "==25.0" },
|
{ name = "packaging", specifier = "==25.0" },
|
||||||
|
{ name = "pillow", specifier = ">=12.0.0" },
|
||||||
{ name = "protobuf", specifier = "==5.29.5" },
|
{ name = "protobuf", specifier = "==5.29.5" },
|
||||||
{ name = "psycopg2-binary", marker = "extra == 'migration'", specifier = "==2.9.10" },
|
{ name = "psycopg2-binary", marker = "extra == 'migration'", specifier = "==2.9.10" },
|
||||||
{ name = "pycryptodomex", specifier = "==3.23.0" },
|
{ name = "pycryptodomex", specifier = "==3.23.0" },
|
||||||
|
|
@ -1778,6 +1786,54 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/08/26e68b6b5da219c2a2cb7b563af008b53bb8e6b6fcb3fa40715fcdb2523a/pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", size = 5289809, upload-time = "2025-10-15T18:21:27.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/e9/4e58fb097fb74c7b4758a680aacd558810a417d1edaa7000142976ef9d2f/pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", size = 4650606, upload-time = "2025-10-15T18:21:29.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e0/1fa492aa9f77b3bc6d471c468e62bfea1823056bf7e5e4f1914d7ab2565e/pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", size = 6221023, upload-time = "2025-10-15T18:21:31.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/09/4de7cd03e33734ccd0c876f0251401f1314e819cbfd89a0fcb6e77927cc6/pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", size = 8024937, upload-time = "2025-10-15T18:21:33.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/69/0688e7c1390666592876d9d474f5e135abb4acb39dcb583c4dc5490f1aff/pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", size = 6334139, upload-time = "2025-10-15T18:21:35.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074, upload-time = "2025-10-15T18:21:37.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852, upload-time = "2025-10-15T18:21:39.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058, upload-time = "2025-10-15T18:21:40.997Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431, upload-time = "2025-10-15T18:21:42.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412, upload-time = "2025-10-15T18:21:44.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903, upload-time = "2025-10-15T18:21:46.29Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.5.0"
|
version = "4.5.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue