"""OTA 升级包存储:Luxsin-X8 -> S3,Luxsin-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: """X8:http://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: """X9:http://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 对外 URL:http://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 Key:ota/{yyyyMM}/x8/{md5前5位}/LUXSIN_X8.PKG 对外 URL:http://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