chore: 补 S4 启用与配置合并的运维脚本
- scripts/patch_env.py:把 KEY=VALUE 文件合并进 .env,只回显键名与长度,不回显密钥 - scripts/check_s4.py:在生产机上验证「预签名 → PUT → 公开读回」整条直传链路 - .env.example:注明本地后端连生产库时 HASHID_SECRET 必须与线上一致 (不一致会把线上 brand_uid 解码成另一个 id,任务被判 brand not found)
This commit is contained in:
94
scripts/check_s4.py
Normal file
94
scripts/check_s4.py
Normal file
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""在生产机上验证 S4 直传链路:签发预签名 URL → PUT 一个小文件 → 公开读回。
|
||||
|
||||
只输出状态与长度,不输出密钥。
|
||||
用法:python3 check_s4.py
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
ENV_PATH = "/opt/fashion/backend_v2/.env"
|
||||
ENDPOINT = "http://127.0.0.1:18092/admin/internal/upload-url"
|
||||
|
||||
|
||||
def load_env(path):
|
||||
env = {}
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
env[key.strip()] = value
|
||||
return env
|
||||
|
||||
|
||||
def main():
|
||||
env = load_env(ENV_PATH)
|
||||
secret = env.get("INGEST_SECRET", "")
|
||||
bucket = env.get("S4_BUCKET", "")
|
||||
if not secret:
|
||||
print("!! .env 里没有 INGEST_SECRET")
|
||||
return 1
|
||||
|
||||
key = f"runway/s4test-{int(time.time())}.jpg"
|
||||
body = json.dumps({"items": [{"key": key, "content_type": "image/jpeg"}]}, separators=(",", ":"))
|
||||
ts = str(int(time.time()))
|
||||
nonce = secrets.token_hex(8)
|
||||
sig = hmac.new(secret.encode(), f"{ts}.{nonce}.{body}".encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
req = urllib.request.Request(
|
||||
ENDPOINT,
|
||||
data=body.encode(),
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Signature": sig,
|
||||
"X-Timestamp": ts,
|
||||
"X-Nonce": nonce,
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
print(f"1) 签发预签名: HTTP {resp.status}")
|
||||
data = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"1) 签发预签名失败: HTTP {exc.code} {exc.read()[:200]!r}")
|
||||
return 1
|
||||
|
||||
item = data["items"][0]
|
||||
# 预签名 URL 里带签名参数,只显示 host 与 path
|
||||
parsed = urllib.request.urlparse(item["url"])
|
||||
print(f" 签名 URL: {parsed.scheme}://{parsed.netloc}{parsed.path[:40]}... 长度={len(item['url'])}")
|
||||
|
||||
payload = b"\xff\xd8\xff\xe0" + b"fashion-s4-probe" + b"\xff\xd9"
|
||||
put = urllib.request.Request(
|
||||
item["url"], data=payload, method="PUT", headers={"Content-Type": "image/jpeg"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(put, timeout=30) as resp:
|
||||
print(f"2) 上传: HTTP {resp.status}")
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"2) 上传失败: HTTP {exc.code} {exc.read()[:300]!r}")
|
||||
return 1
|
||||
|
||||
public_url = f"https://{bucket}.s3.bitiful.net/{key}"
|
||||
try:
|
||||
with urllib.request.urlopen(public_url, timeout=20) as resp:
|
||||
got = resp.read()
|
||||
print(f"3) 公开读取: HTTP {resp.status} 字节={len(got)} 一致={got == payload}")
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(f"3) 公开读取失败: HTTP {exc.code}(桶可能不是公开读)")
|
||||
return 1
|
||||
|
||||
print(f" 测试对象: {key}({len(payload)} 字节,可忽略)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
53
scripts/patch_env.py
Normal file
53
scripts/patch_env.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把 KEY=VALUE 文件里的键合并进目标 .env(存在则替换,不存在则追加)。
|
||||
|
||||
用途:在不把密钥回显到终端的前提下,更新服务器上的生产配置。
|
||||
用法:python3 patch_env.py <kv文件> <目标.env>
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print("用法: patch_env.py <kv文件> <目标.env>")
|
||||
return 2
|
||||
kv_path, env_path = sys.argv[1], sys.argv[2]
|
||||
|
||||
kv = {}
|
||||
with open(kv_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or "=" not in line or line.startswith("#"):
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
kv[key.strip()] = value
|
||||
|
||||
with open(env_path, encoding="utf-8") as fh:
|
||||
lines = fh.read().splitlines()
|
||||
|
||||
seen = set()
|
||||
out = []
|
||||
for line in lines:
|
||||
m = re.match(r"^([A-Z0-9_]+)=", line)
|
||||
if m and m.group(1) in kv:
|
||||
key = m.group(1)
|
||||
out.append(f"{key}={kv[key]}")
|
||||
seen.add(key)
|
||||
else:
|
||||
out.append(line)
|
||||
for key, value in kv.items():
|
||||
if key not in seen:
|
||||
out.append(f"{key}={value}")
|
||||
|
||||
with open(env_path, "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(out) + "\n")
|
||||
|
||||
# 只打印键名与长度,绝不打印值
|
||||
for key in sorted(kv):
|
||||
print(f" {key}: {'已更新' if key in seen else '已追加'} 长度={len(kv[key])}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user