278 lines
9.1 KiB
Python
278 lines
9.1 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
图片迁移到新 OSS 存储桶
|
|||
|
|
集成配置和上传功能
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import oss2
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
# 本地图片目录
|
|||
|
|
GIFT_DIR = 'public/images/gifts'
|
|||
|
|
OUTFIT_DIR = 'public/images/outfits'
|
|||
|
|
|
|||
|
|
# OSS 上传路径(保持与数据库一致)
|
|||
|
|
OSS_GIFT_PREFIX = 'uploads/gifts/'
|
|||
|
|
OSS_OUTFIT_PREFIX = 'uploads/outfit/'
|
|||
|
|
|
|||
|
|
class OSSMigrator:
|
|||
|
|
def __init__(self):
|
|||
|
|
self.config = {}
|
|||
|
|
self.bucket = None
|
|||
|
|
|
|||
|
|
def get_input(self, prompt, default=None):
|
|||
|
|
"""获取用户输入"""
|
|||
|
|
if default:
|
|||
|
|
prompt = f"{prompt} [{default}]: "
|
|||
|
|
else:
|
|||
|
|
prompt = f"{prompt}: "
|
|||
|
|
|
|||
|
|
value = input(prompt).strip()
|
|||
|
|
return value if value else default
|
|||
|
|
|
|||
|
|
def validate_endpoint(self, endpoint):
|
|||
|
|
"""验证 Endpoint 格式"""
|
|||
|
|
endpoint = endpoint.replace('https://', '').replace('http://', '')
|
|||
|
|
return endpoint
|
|||
|
|
|
|||
|
|
def configure(self):
|
|||
|
|
"""配置新 OSS 信息"""
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("配置新的 OSS 存储桶")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("\n请输入新的 OSS 配置信息:\n")
|
|||
|
|
|
|||
|
|
# 获取配置信息
|
|||
|
|
self.config['access_key_id'] = self.get_input("Access Key ID")
|
|||
|
|
if not self.config['access_key_id']:
|
|||
|
|
print("✗ Access Key ID 不能为空")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self.config['access_key_secret'] = self.get_input("Access Key Secret")
|
|||
|
|
if not self.config['access_key_secret']:
|
|||
|
|
print("✗ Access Key Secret 不能为空")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self.config['bucket_name'] = self.get_input("Bucket 名称")
|
|||
|
|
if not self.config['bucket_name']:
|
|||
|
|
print("✗ Bucket 名称不能为空")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self.config['endpoint'] = self.get_input("Endpoint", "oss-cn-hangzhou.aliyuncs.com")
|
|||
|
|
self.config['endpoint'] = self.validate_endpoint(self.config['endpoint'])
|
|||
|
|
|
|||
|
|
# 显示配置摘要
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("配置摘要")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"Access Key ID: {self.config['access_key_id'][:8]}...")
|
|||
|
|
print(f"Access Key Secret: {self.config['access_key_secret'][:8]}...")
|
|||
|
|
print(f"Bucket 名称: {self.config['bucket_name']}")
|
|||
|
|
print(f"Endpoint: {self.config['endpoint']}")
|
|||
|
|
print(f"CDN 域名: https://{self.config['bucket_name']}.{self.config['endpoint']}")
|
|||
|
|
|
|||
|
|
# 确认
|
|||
|
|
confirm = self.get_input("\n确认配置?(y/n)", "y")
|
|||
|
|
if confirm.lower() != 'y':
|
|||
|
|
print("已取消")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def init_oss_bucket(self):
|
|||
|
|
"""初始化 OSS 连接"""
|
|||
|
|
try:
|
|||
|
|
auth = oss2.Auth(self.config['access_key_id'], self.config['access_key_secret'])
|
|||
|
|
self.bucket = oss2.Bucket(auth, self.config['endpoint'], self.config['bucket_name'])
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"✗ 初始化 OSS 失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def test_oss_connection(self):
|
|||
|
|
"""测试 OSS 连接"""
|
|||
|
|
try:
|
|||
|
|
for obj in oss2.ObjectIterator(self.bucket, max_keys=1):
|
|||
|
|
pass
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"✗ OSS 连接失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def upload_file(self, local_path, oss_path):
|
|||
|
|
"""上传单个文件到 OSS"""
|
|||
|
|
try:
|
|||
|
|
result = self.bucket.put_object_from_file(oss_path, local_path)
|
|||
|
|
if result.status == 200:
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
print(f" ✗ 上传失败 (状态码: {result.status})")
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ✗ 上传错误: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def upload_gifts(self):
|
|||
|
|
"""上传礼物图片"""
|
|||
|
|
print(f"\n📦 上传礼物图片")
|
|||
|
|
print(f"本地目录: {GIFT_DIR}")
|
|||
|
|
print(f"OSS 路径: {OSS_GIFT_PREFIX}")
|
|||
|
|
|
|||
|
|
if not os.path.exists(GIFT_DIR):
|
|||
|
|
print(f"✗ 目录不存在: {GIFT_DIR}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
files = [f for f in os.listdir(GIFT_DIR) if f.endswith(('.png', '.jpg', '.jpeg', '.gif'))]
|
|||
|
|
print(f"找到 {len(files)} 个文件")
|
|||
|
|
|
|||
|
|
success = 0
|
|||
|
|
failed = 0
|
|||
|
|
skipped = 0
|
|||
|
|
|
|||
|
|
for filename in files:
|
|||
|
|
local_path = os.path.join(GIFT_DIR, filename)
|
|||
|
|
oss_path = OSS_GIFT_PREFIX + filename
|
|||
|
|
|
|||
|
|
# 检查文件是否已存在
|
|||
|
|
try:
|
|||
|
|
self.bucket.head_object(oss_path)
|
|||
|
|
print(f" ○ 已存在: {filename}")
|
|||
|
|
skipped += 1
|
|||
|
|
continue
|
|||
|
|
except oss2.exceptions.NoSuchKey:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
print(f" ↑ 上传: {filename}")
|
|||
|
|
if self.upload_file(local_path, oss_path):
|
|||
|
|
success += 1
|
|||
|
|
print(f" ✓ 成功")
|
|||
|
|
else:
|
|||
|
|
failed += 1
|
|||
|
|
|
|||
|
|
print(f"\n礼物上传完成: 成功 {success}, 失败 {failed}, 跳过 {skipped}")
|
|||
|
|
|
|||
|
|
def upload_outfits(self):
|
|||
|
|
"""上传服装图片"""
|
|||
|
|
print(f"\n👔 上传服装图片")
|
|||
|
|
print(f"本地目录: {OUTFIT_DIR}")
|
|||
|
|
print(f"OSS 路径: {OSS_OUTFIT_PREFIX}")
|
|||
|
|
|
|||
|
|
if not os.path.exists(OUTFIT_DIR):
|
|||
|
|
print(f"✗ 目录不存在: {OUTFIT_DIR}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
files = [f for f in os.listdir(OUTFIT_DIR) if f.endswith(('.png', '.jpg', '.jpeg', '.gif'))]
|
|||
|
|
print(f"找到 {len(files)} 个文件")
|
|||
|
|
|
|||
|
|
success = 0
|
|||
|
|
failed = 0
|
|||
|
|
skipped = 0
|
|||
|
|
|
|||
|
|
# 服装图片需要按类型分类上传
|
|||
|
|
outfit_types = {
|
|||
|
|
'top': ['tshirt', 'shirt', 'sleeve', 'strap', 'shoulder', 'crop', 'cardigan', 'jacket', 'sweatshirt'],
|
|||
|
|
'bottom': ['jeans', 'skirt', 'shorts', 'pants', 'sweatpants'],
|
|||
|
|
'dress': ['dress', 'uniform', 'hanfu', 'lolita']
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for filename in files:
|
|||
|
|
local_path = os.path.join(OUTFIT_DIR, filename)
|
|||
|
|
|
|||
|
|
# 判断服装类型
|
|||
|
|
outfit_type = 'dress'
|
|||
|
|
for type_name, keywords in outfit_types.items():
|
|||
|
|
if any(keyword in filename.lower() for keyword in keywords):
|
|||
|
|
outfit_type = type_name
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
oss_path = f"{OSS_OUTFIT_PREFIX}{outfit_type}/{filename}"
|
|||
|
|
|
|||
|
|
# 检查文件是否已存在
|
|||
|
|
try:
|
|||
|
|
self.bucket.head_object(oss_path)
|
|||
|
|
print(f" ○ 已存在: {filename} ({outfit_type})")
|
|||
|
|
skipped += 1
|
|||
|
|
continue
|
|||
|
|
except oss2.exceptions.NoSuchKey:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
print(f" ↑ 上传: {filename} -> {outfit_type}/")
|
|||
|
|
if self.upload_file(local_path, oss_path):
|
|||
|
|
success += 1
|
|||
|
|
print(f" ✓ 成功")
|
|||
|
|
else:
|
|||
|
|
failed += 1
|
|||
|
|
|
|||
|
|
print(f"\n服装上传完成: 成功 {success}, 失败 {failed}, 跳过 {skipped}")
|
|||
|
|
|
|||
|
|
def generate_env_config(self):
|
|||
|
|
"""生成 .env 配置内容"""
|
|||
|
|
endpoint_with_https = f"https://{self.config['endpoint']}"
|
|||
|
|
cdn_domain = f"https://{self.config['bucket_name']}.{self.config['endpoint']}"
|
|||
|
|
|
|||
|
|
env_content = f"""
|
|||
|
|
# ===== 新的 OSS 配置 =====
|
|||
|
|
ALIYUN_OSS_ACCESS_KEY_ID={self.config['access_key_id']}
|
|||
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET={self.config['access_key_secret']}
|
|||
|
|
ALIYUN_OSS_BUCKET_NAME={self.config['bucket_name']}
|
|||
|
|
ALIYUN_OSS_ENDPOINT={endpoint_with_https}
|
|||
|
|
ALIYUN_OSS_CDN_DOMAIN={cdn_domain}
|
|||
|
|
"""
|
|||
|
|
return env_content
|
|||
|
|
|
|||
|
|
def run(self):
|
|||
|
|
"""运行迁移流程"""
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("图片迁移到新 OSS 存储桶")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
# 步骤1: 配置
|
|||
|
|
if not self.configure():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 步骤2: 初始化 OSS
|
|||
|
|
print("\n正在连接 OSS...")
|
|||
|
|
if not self.init_oss_bucket():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 步骤3: 测试连接
|
|||
|
|
if not self.test_oss_connection():
|
|||
|
|
print("\n✗ 无法连接到 OSS,请检查配置")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
print("✓ OSS 连接成功")
|
|||
|
|
|
|||
|
|
# 步骤4: 上传礼物图片
|
|||
|
|
self.upload_gifts()
|
|||
|
|
|
|||
|
|
# 步骤5: 上传服装图片
|
|||
|
|
self.upload_outfits()
|
|||
|
|
|
|||
|
|
# 步骤6: 显示后续操作
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("✓ 上传完成!")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("\n下一步操作:")
|
|||
|
|
print("\n1. 将以下内容添加到 .env 文件:")
|
|||
|
|
print(self.generate_env_config())
|
|||
|
|
print("\n2. 重启服务:")
|
|||
|
|
print(" 开发\\1-启动项目.bat")
|
|||
|
|
print("\n数据库无需修改,图片路径保持不变!")
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
try:
|
|||
|
|
migrator = OSSMigrator()
|
|||
|
|
migrator.run()
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
print("\n\n用户中断操作")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"\n✗ 发生错误: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
main()
|