Files
dashboard/backend/ota_storage.py
T

125 lines
4.3 KiB
Python
Raw Normal View History

"""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(具体目录在 get_ota_upload_dir 内按当前环境读取)
_OTA_UPLOAD_DIR_DEV_DEFAULT = "H:/soft/projects/luxsin/dashboard/ota"
_OTA_UPLOAD_DIR_PROD_DEFAULT = "/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:
"""按 DEBUG 选开发/生产目录;每次调用重新读环境变量,避免 import 早于 load_dotenv。"""
debug_val = (os.getenv("DEBUG") or "false").strip().lower()
is_dev = debug_val in ("true", "1", "yes", "on")
dev = os.getenv("OTA_UPLOAD_DIR_DEV", _OTA_UPLOAD_DIR_DEV_DEFAULT)
prod = os.getenv("OTA_UPLOAD_DIR_PROD", _OTA_UPLOAD_DIR_PROD_DEFAULT)
raw = dev if is_dev else 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