Files
dashboard/backend/ota_storage.py
T
yangy 1a6c96da8c Add OTA package upload functionality and UI enhancements
- Introduced a new endpoint for uploading OTA packages in backend/routes/ota.py.
- Implemented file handling and MD5 checksum calculation for OTA packages.
- Updated frontend API to support OTA package uploads with a new uploadOtaPackage function.
- Enhanced the OTA management UI to include file upload options and improved layout.
- Updated requirements.txt to include boto3 for S3 interactions.
- Made various UI adjustments for better user experience and consistency.
2026-05-15 17:34:54 +08:00

125 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OTA 升级包存储:Luxsin-X8 -> S3Luxsin-X9 -> 本地目录"""
import hashlib
import io
import logging
import os
from datetime import datetime
from pathlib import Path
import boto3
from botocore.exceptions import ClientError
from fastapi import UploadFile
logger = logging.getLogger(__name__)
OTA_MODEL_X8 = "Luxsin-X8"
OTA_MODEL_X9 = "Luxsin-X9"
OTA_UPLOAD_MODELS = {OTA_MODEL_X8, OTA_MODEL_X9}
# 本地根目录;实际文件路径为 {根}/{md5前5位}/LUXSIN.PKG
OTA_UPLOAD_DIR_DEV = os.getenv(
"OTA_UPLOAD_DIR_DEV", "H:/soft/projects/luxsin/dashboard/ota"
)
OTA_UPLOAD_DIR_PROD = os.getenv(
"OTA_UPLOAD_DIR_PROD", "/data/project/dashboard/upload"
)
AWS_REGION = os.getenv("AWS_REGION", "eu-central-1")
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")
AWS_S3_OTA_BUCKET = os.getenv("AWS_S3_OTA_BUCKET", "luxsin-app-bucket")
OTA_X8_PUBLIC_BASE = (os.getenv("OTA_X8_PUBLIC_BASE") or "http://am.luxsinaudio.com").rstrip(
"/"
)
OTA_X9_URL_BASE = (os.getenv("OTA_X9_URL_BASE") or "http://source.luxsin.net").rstrip("/")
OTA_FILENAME_X8 = "LUXSIN_X8.PKG"
OTA_FILENAME_X9 = "LUXSIN.PKG"
def get_ota_upload_dir() -> Path:
is_dev = os.getenv("DEBUG", "False").lower() in ("true", "1", "yes")
raw = OTA_UPLOAD_DIR_DEV if is_dev else OTA_UPLOAD_DIR_PROD
return Path(raw)
def md5_prefix5(md5_hex: str) -> str:
return (md5_hex or "")[:5]
def build_x8_public_url(s3_key: str) -> str:
"""X8http://am.luxsinaudio.com/ota/{yyyyMM}/x8/{md5前5位}/LUXSIN_X8.PKG"""
return f"{OTA_X8_PUBLIC_BASE}/{s3_key}"
def build_x9_public_url(ym: str, prefix: str) -> str:
"""X9http://source.luxsin.net/ota/{yyyyMM}/x9/{md5前5位}/LUXSIN.PKG"""
return f"{OTA_X9_URL_BASE}/ota/{ym}/x9/{prefix}/{OTA_FILENAME_X9}"
async def read_upload_content_and_md5(package_file: UploadFile) -> tuple[bytes, str]:
md5_hash = hashlib.md5()
buf = io.BytesIO()
chunk_size = 1024 * 1024
while True:
chunk = await package_file.read(chunk_size)
if not chunk:
break
md5_hash.update(chunk)
buf.write(chunk)
return buf.getvalue(), md5_hash.hexdigest()
def save_x9_package_local(content: bytes, md5_hex: str) -> tuple[str, str]:
"""
保存到 {OTA_UPLOAD_DIR}/{md5前5位}/LUXSIN.PKG
对外 URLhttp://source.luxsin.net/ota/{yyyyMM}/x9/{md5前5位}/LUXSIN.PKG
"""
root = get_ota_upload_dir()
prefix = md5_prefix5(md5_hex)
ym = datetime.now().strftime("%Y%m")
dest_dir = root / prefix
dest_dir.mkdir(parents=True, exist_ok=True)
final_path = dest_dir / OTA_FILENAME_X9
if final_path.exists():
final_path.unlink()
final_path.write_bytes(content)
download_url = build_x9_public_url(ym, prefix)
logger.info("OTA X9 package saved locally: %s", final_path)
return OTA_FILENAME_X9, download_url
def upload_x8_package_to_s3(content: bytes, md5_hex: str) -> tuple[str, str, str]:
"""
S3 Keyota/{yyyyMM}/x8/{md5前5位}/LUXSIN_X8.PKG
对外 URLhttp://am.luxsinaudio.com/ota/{yyyyMM}/x8/{md5前5位}/LUXSIN_X8.PKG
"""
if not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY:
raise ValueError("未配置 AWS 访问密钥(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY")
ym = datetime.now().strftime("%Y%m")
prefix = md5_prefix5(md5_hex)
s3_key = f"ota/{ym}/x8/{prefix}/{OTA_FILENAME_X8}"
client = boto3.client(
"s3",
region_name=AWS_REGION,
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
)
try:
client.put_object(
Bucket=AWS_S3_OTA_BUCKET,
Key=s3_key,
Body=content,
ContentType="application/octet-stream",
)
except ClientError as e:
logger.error("S3 upload failed: %s", e, exc_info=True)
raise ValueError(f"S3 上传失败:{e}") from e
download_url = build_x8_public_url(s3_key)
logger.info("OTA X8 package uploaded to s3://%s/%s", AWS_S3_OTA_BUCKET, s3_key)
return OTA_FILENAME_X8, download_url, s3_key