542 lines
19 KiB
Python
542 lines
19 KiB
Python
from minio import Minio
|
||
from minio.error import S3Error
|
||
import json
|
||
import os
|
||
import numpy as np
|
||
from urllib.parse import urlparse
|
||
import hashlib
|
||
import time
|
||
import re
|
||
import pickle
|
||
from datetime import datetime
|
||
|
||
class MinIO3DTilesManager:
|
||
def __init__(self, endpoint_url, access_key, secret_key, secure=False,
|
||
mapping_file="minio_path_mapping.pkl"):
|
||
"""
|
||
初始化MinIO客户端
|
||
|
||
Args:
|
||
endpoint_url: MinIO服务地址 (如: 222.212.85.86:9001)
|
||
access_key: 访问密钥
|
||
secret_key: 秘密密钥
|
||
secure: 是否使用HTTPS
|
||
mapping_file: 路径映射文件名
|
||
"""
|
||
if endpoint_url.startswith('http://'):
|
||
endpoint_url = endpoint_url.replace('http://', '')
|
||
elif endpoint_url.startswith('https://'):
|
||
endpoint_url = endpoint_url.replace('https://', '')
|
||
secure = True
|
||
|
||
self.endpoint_url = endpoint_url
|
||
self.access_key = access_key
|
||
self.secret_key = secret_key
|
||
|
||
self.minio_client = Minio(
|
||
endpoint_url,
|
||
access_key=access_key,
|
||
secret_key=secret_key,
|
||
secure=secure
|
||
)
|
||
|
||
# 获取脚本所在目录
|
||
self.script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 映射文件路径
|
||
self.mapping_file = os.path.join(self.script_dir, mapping_file)
|
||
|
||
# 加载现有的路径映射
|
||
self.path_mapping = self.load_path_mapping()
|
||
|
||
def load_path_mapping(self):
|
||
"""加载路径映射数据"""
|
||
if os.path.exists(self.mapping_file):
|
||
try:
|
||
with open(self.mapping_file, 'rb') as f:
|
||
mapping = pickle.load(f)
|
||
return mapping
|
||
except Exception as e:
|
||
return {}
|
||
else:
|
||
return {}
|
||
|
||
def save_path_mapping(self):
|
||
"""保存路径映射数据"""
|
||
try:
|
||
with open(self.mapping_file, 'wb') as f:
|
||
pickle.dump(self.path_mapping, f)
|
||
return True
|
||
except Exception as e:
|
||
return False
|
||
|
||
def get_cache_key(self, tileset_url, save_dir=None):
|
||
"""生成缓存键"""
|
||
# 基于URL和保存目录生成缓存键
|
||
cache_data = f"{tileset_url}|{save_dir}"
|
||
return hashlib.md5(cache_data.encode()).hexdigest()
|
||
|
||
def get_cached_tileset_info(self, tileset_url, save_dir=None):
|
||
"""获取缓存的tileset信息"""
|
||
cache_key = self.get_cache_key(tileset_url, save_dir)
|
||
|
||
# 检查缓存映射中是否有这个tileset
|
||
for file_id, info in self.path_mapping.items():
|
||
if info.get('cache_key') == cache_key and info.get('is_tileset_root'):
|
||
# 检查入口文件是否存在
|
||
local_path = info.get('local_path')
|
||
if local_path and os.path.exists(local_path):
|
||
return local_path
|
||
return None
|
||
|
||
def update_tileset_cache(self, tileset_url, save_dir, local_path):
|
||
"""更新tileset缓存信息"""
|
||
cache_key = self.get_cache_key(tileset_url, save_dir)
|
||
|
||
# 将tileset根文件标记为缓存
|
||
entry_bucket, entry_path = self.parse_minio_url(tileset_url)
|
||
file_id = f"{entry_bucket}/{entry_path}"
|
||
|
||
if file_id in self.path_mapping:
|
||
self.path_mapping[file_id]['cache_key'] = cache_key
|
||
self.path_mapping[file_id]['is_tileset_root'] = True
|
||
self.path_mapping[file_id]['tileset_url'] = tileset_url
|
||
self.path_mapping[file_id]['save_dir'] = save_dir
|
||
self.path_mapping[file_id]['cache_time'] = datetime.now().isoformat()
|
||
|
||
def download_full_tileset(self, tileset_url, save_dir=None, region_filter=None, use_cache=True):
|
||
"""
|
||
下载完整的3D Tiles数据集,支持缓存功能
|
||
|
||
Args:
|
||
tileset_url: MinIO上的tileset.json URL
|
||
save_dir: 本地保存目录
|
||
region_filter: 区域过滤器
|
||
use_cache: 是否使用缓存
|
||
|
||
Returns:
|
||
tuple: (success, result)
|
||
- success: True/False
|
||
- result: 如果success=True且use_cache=True,返回本地路径;否则返回True/False
|
||
"""
|
||
if save_dir is None:
|
||
save_dir = os.path.join(self.script_dir, "data_3dtiles")
|
||
|
||
# 清理保存目录名称
|
||
save_dir = self.clean_file_path(save_dir)
|
||
|
||
# 检查缓存:只需检查入口文件是否存在
|
||
if use_cache:
|
||
cached_path = self.get_cached_tileset_info(tileset_url, save_dir)
|
||
if cached_path:
|
||
# 入口文件存在,默认缓存完备
|
||
return True, cached_path
|
||
|
||
# 解析URL
|
||
entry_bucket, entry_path = self.parse_minio_url(tileset_url)
|
||
if not entry_bucket or not entry_path:
|
||
return False, "无法解析URL"
|
||
|
||
entry_dir = os.path.dirname(entry_path)
|
||
|
||
# 创建保存目录
|
||
os.makedirs(save_dir, exist_ok=True)
|
||
|
||
visited = set()
|
||
|
||
# 下载入口文件
|
||
entry_local_path = self.get_local_path(entry_bucket, entry_path, save_dir)
|
||
|
||
success, result = self.download_file(entry_bucket, entry_path, entry_local_path)
|
||
if not success:
|
||
return False, f"入口文件下载失败: {result}"
|
||
|
||
entry_id = f"{entry_bucket}/{entry_path}"
|
||
visited.add(entry_id)
|
||
|
||
# 加载tileset数据
|
||
tileset_data = self.load_json_from_minio(entry_bucket, entry_path)
|
||
if not tileset_data or "root" not in tileset_data:
|
||
return False, "无效的tileset.json文件"
|
||
|
||
# 遍历下载所有文件
|
||
self.traverse_and_download_tileset(
|
||
tileset_data["root"],
|
||
entry_bucket,
|
||
entry_dir,
|
||
entry_bucket,
|
||
entry_dir,
|
||
save_dir,
|
||
region_filter,
|
||
None,
|
||
visited
|
||
)
|
||
|
||
# 更新缓存信息
|
||
self.update_tileset_cache(tileset_url, save_dir, entry_local_path)
|
||
|
||
# 保存路径映射
|
||
self.save_path_mapping()
|
||
|
||
if use_cache:
|
||
return True, entry_local_path
|
||
else:
|
||
return True, True
|
||
|
||
def get_tileset_local_path(self, tileset_url, save_dir=None):
|
||
"""
|
||
获取已缓存的tileset本地路径
|
||
|
||
Args:
|
||
tileset_url: tileset的URL
|
||
save_dir: 保存目录
|
||
|
||
Returns:
|
||
str: 本地路径,如果未缓存则返回None
|
||
"""
|
||
if save_dir is None:
|
||
save_dir = os.path.join(self.script_dir, "data_3dtiles")
|
||
|
||
return self.get_cached_tileset_info(tileset_url, save_dir)
|
||
|
||
def clear_tileset_cache(self, tileset_url=None, save_dir=None):
|
||
"""
|
||
清除tileset缓存
|
||
|
||
Args:
|
||
tileset_url: 指定要清除的tileset URL,如果为None则清除所有
|
||
save_dir: 保存目录
|
||
|
||
Returns:
|
||
bool: 成功/失败
|
||
"""
|
||
try:
|
||
if tileset_url:
|
||
# 清除指定tileset的缓存
|
||
cache_key = self.get_cache_key(tileset_url, save_dir)
|
||
|
||
# 找出所有相关的缓存条目
|
||
to_remove = []
|
||
for file_id, info in self.path_mapping.items():
|
||
if info.get('cache_key') == cache_key:
|
||
to_remove.append(file_id)
|
||
|
||
# 删除这些条目
|
||
for file_id in to_remove:
|
||
del self.path_mapping[file_id]
|
||
|
||
print(f"已清除tileset缓存: {tileset_url}")
|
||
else:
|
||
# 清除所有缓存
|
||
self.path_mapping = {}
|
||
if os.path.exists(self.mapping_file):
|
||
os.remove(self.mapping_file)
|
||
print("已清除所有缓存")
|
||
|
||
return True
|
||
except Exception as e:
|
||
return False
|
||
|
||
# 以下是原有的辅助方法
|
||
def clean_filename(self, filename):
|
||
"""清理文件名中的特殊字符"""
|
||
if not filename:
|
||
return ""
|
||
cleaned = re.sub(r'[<>:"/\\|?*\x00-\x1F]', '_', filename)
|
||
cleaned = re.sub(r'_+', '_', cleaned)
|
||
cleaned = cleaned.strip(' _')
|
||
return cleaned
|
||
|
||
def parse_minio_url(self, url):
|
||
"""解析MinIO URL"""
|
||
if url.startswith('http://') or url.startswith('https://'):
|
||
parsed = urlparse(url)
|
||
path = parsed.path.lstrip('/')
|
||
parts = path.split('/', 1)
|
||
if len(parts) == 2:
|
||
bucket, key = parts
|
||
else:
|
||
bucket = parts[0]
|
||
key = ""
|
||
return bucket, key
|
||
else:
|
||
parts = url.split('/', 1)
|
||
if len(parts) == 2:
|
||
bucket, key = parts
|
||
else:
|
||
bucket = parts[0]
|
||
key = ""
|
||
return bucket, key
|
||
|
||
def download_file(self, bucket_name, object_name, file_path):
|
||
"""从MinIO下载文件"""
|
||
try:
|
||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||
|
||
# 清理文件名
|
||
clean_file_path = self.clean_file_path(file_path)
|
||
|
||
# 检查是否已下载
|
||
file_id = f"{bucket_name}/{object_name}"
|
||
if file_id in self.path_mapping:
|
||
mapped_path = self.path_mapping[file_id]['local_path']
|
||
if os.path.exists(mapped_path):
|
||
return True, mapped_path
|
||
|
||
# 下载文件
|
||
self.minio_client.fget_object(
|
||
bucket_name,
|
||
object_name,
|
||
clean_file_path
|
||
)
|
||
|
||
# 更新路径映射
|
||
self.path_mapping[file_id] = {
|
||
'local_path': clean_file_path,
|
||
'bucket': bucket_name,
|
||
'object': object_name,
|
||
'download_time': datetime.now().isoformat(),
|
||
'size': os.path.getsize(clean_file_path)
|
||
}
|
||
|
||
return True, clean_file_path
|
||
|
||
except S3Error as e:
|
||
return False, str(e)
|
||
except Exception as e:
|
||
return False, str(e)
|
||
|
||
def clean_file_path(self, file_path):
|
||
"""清理文件路径中的所有特殊字符"""
|
||
dir_name = os.path.dirname(file_path)
|
||
file_name = os.path.basename(file_path)
|
||
|
||
if dir_name:
|
||
dir_parts = dir_name.split(os.sep)
|
||
cleaned_parts = []
|
||
for part in dir_parts:
|
||
cleaned_part = self.clean_filename(part)
|
||
if cleaned_part:
|
||
cleaned_parts.append(cleaned_part)
|
||
cleaned_dir = os.sep.join(cleaned_parts)
|
||
else:
|
||
cleaned_dir = ""
|
||
|
||
cleaned_file = self.clean_filename(file_name)
|
||
|
||
if cleaned_dir:
|
||
cleaned_path = os.path.join(cleaned_dir, cleaned_file)
|
||
else:
|
||
cleaned_path = cleaned_file
|
||
|
||
return cleaned_path
|
||
|
||
def load_json_from_minio(self, bucket_name, object_name):
|
||
"""从MinIO加载JSON文件"""
|
||
try:
|
||
self.minio_client.stat_object(bucket_name, object_name)
|
||
|
||
response = self.minio_client.get_object(bucket_name, object_name)
|
||
content = response.read().decode('utf-8')
|
||
response.close()
|
||
response.release_conn()
|
||
|
||
return json.loads(content)
|
||
|
||
except S3Error as e:
|
||
return None
|
||
except Exception as e:
|
||
return None
|
||
|
||
def get_local_path(self, bucket_name, object_name, save_dir):
|
||
"""生成保持目录结构的本地路径"""
|
||
clean_bucket = self.clean_filename(bucket_name)
|
||
|
||
path_parts = object_name.split('/')
|
||
cleaned_parts = []
|
||
for part in path_parts:
|
||
cleaned_part = self.clean_filename(part)
|
||
if cleaned_part:
|
||
cleaned_parts.append(cleaned_part)
|
||
|
||
if cleaned_parts:
|
||
cleaned_relative = '/'.join(cleaned_parts)
|
||
local_path = os.path.join(save_dir, clean_bucket, cleaned_relative)
|
||
else:
|
||
local_path = os.path.join(save_dir, clean_bucket)
|
||
|
||
|
||
return os.path.normpath(local_path)
|
||
|
||
def traverse_and_download_tileset(self, tile_obj, current_bucket, current_dir,
|
||
base_bucket, base_dir, save_dir,
|
||
region_filter=None, parent_transform=None,
|
||
visited=None):
|
||
"""递归遍历并下载3D Tiles文件"""
|
||
if visited is None:
|
||
visited = set()
|
||
|
||
current_transform = parent_transform
|
||
if "transform" in tile_obj:
|
||
tile_mat = tile_obj["transform"]
|
||
if current_transform is None:
|
||
current_transform = tile_mat
|
||
else:
|
||
mat1 = np.array(current_transform).reshape(4, 4)
|
||
mat2 = np.array(tile_mat).reshape(4, 4)
|
||
combined_mat = np.dot(mat1, mat2).flatten().tolist()
|
||
current_transform = combined_mat
|
||
|
||
skip_current_tile = False
|
||
if region_filter and "boundingVolume" in tile_obj:
|
||
if not region_filter.check_tile_bounding_volume(tile_obj["boundingVolume"]):
|
||
skip_current_tile = True
|
||
|
||
if not skip_current_tile and "content" in tile_obj and "uri" in tile_obj["content"]:
|
||
tile_uri = tile_obj["content"]["uri"]
|
||
|
||
file_bucket = current_bucket
|
||
file_path = ""
|
||
|
||
if tile_uri.startswith('http://') or tile_uri.startswith('https://'):
|
||
parsed_bucket, parsed_path = self.parse_minio_url(tile_uri)
|
||
if parsed_bucket:
|
||
file_bucket = parsed_bucket
|
||
file_path = parsed_path
|
||
else:
|
||
if current_dir:
|
||
file_path = os.path.join(current_dir, tile_uri).replace('\\', '/')
|
||
else:
|
||
file_path = tile_uri
|
||
|
||
file_path = file_path.lstrip('/')
|
||
|
||
file_id = f"{file_bucket}/{file_path}"
|
||
|
||
if file_id not in visited:
|
||
print(f"下载文件:{file_id}")
|
||
visited.add(file_id)
|
||
|
||
local_path = self.get_local_path(file_bucket, file_path, save_dir)
|
||
|
||
self.download_file(file_bucket, file_path, local_path)
|
||
|
||
if file_path.lower().endswith('.json'):
|
||
sub_tileset = self.load_json_from_minio(file_bucket, file_path)
|
||
if sub_tileset and "root" in sub_tileset:
|
||
sub_dir = os.path.dirname(file_path) if file_path else ""
|
||
self.traverse_and_download_tileset(
|
||
sub_tileset["root"],
|
||
file_bucket,
|
||
sub_dir,
|
||
base_bucket,
|
||
base_dir,
|
||
save_dir,
|
||
region_filter,
|
||
current_transform,
|
||
visited
|
||
)
|
||
|
||
if "children" in tile_obj:
|
||
for child_tile in tile_obj["children"]:
|
||
self.traverse_and_download_tileset(
|
||
child_tile,
|
||
current_bucket,
|
||
current_dir,
|
||
base_bucket,
|
||
base_dir,
|
||
save_dir,
|
||
region_filter,
|
||
current_transform,
|
||
visited
|
||
)
|
||
|
||
def upload_file(self, bucket_name, object_name, file_path):
|
||
"""上传文件到MinIO"""
|
||
try:
|
||
if not os.path.exists(file_path):
|
||
return False, f"文件不存在: {file_path}"
|
||
|
||
file_size = os.path.getsize(file_path)
|
||
self.minio_client.fput_object(bucket_name, object_name, file_path)
|
||
|
||
return True, f"{bucket_name}/{object_name}"
|
||
|
||
except S3Error as e:
|
||
return False, f"MinIO上传错误: {e}"
|
||
except Exception as e:
|
||
return False, f"上传失败: {str(e)}"
|
||
|
||
def upload_directory(self, bucket_name, local_dir, remote_prefix=""):
|
||
"""上传目录到MinIO"""
|
||
if not os.path.exists(local_dir):
|
||
return [], [f"目录不存在: {local_dir}"]
|
||
|
||
uploaded_files = []
|
||
failed_files = []
|
||
|
||
for root, dirs, files in os.walk(local_dir):
|
||
for file in files:
|
||
local_path = os.path.join(root, file)
|
||
rel_path = os.path.relpath(local_path, local_dir)
|
||
if remote_prefix:
|
||
remote_path = os.path.join(remote_prefix, rel_path).replace('\\', '/')
|
||
else:
|
||
remote_path = rel_path.replace('\\', '/')
|
||
|
||
success, message = self.upload_file(bucket_name, remote_path, local_path)
|
||
if success:
|
||
uploaded_files.append(remote_path)
|
||
else:
|
||
failed_files.append((remote_path, message))
|
||
|
||
return uploaded_files, failed_files
|
||
|
||
def check_and_create_bucket(self, bucket_name):
|
||
"""检查并创建bucket"""
|
||
try:
|
||
if not self.minio_client.bucket_exists(bucket_name):
|
||
self.minio_client.make_bucket(bucket_name)
|
||
return True, f"创建bucket: {bucket_name}"
|
||
return True, f"bucket已存在: {bucket_name}"
|
||
except S3Error as e:
|
||
return False, f"创建bucket失败: {e}"
|
||
|
||
|
||
# 使用示例
|
||
if __name__ == "__main__":
|
||
# 配置参数
|
||
ENDPOINT_URL = "222.212.85.86:9000"
|
||
ACCESS_KEY = "WuRenJi"
|
||
SECRET_KEY = "WRJ@2024"
|
||
|
||
# 初始化管理器
|
||
manager = MinIO3DTilesManager(
|
||
endpoint_url=ENDPOINT_URL,
|
||
access_key=ACCESS_KEY,
|
||
secret_key=SECRET_KEY,
|
||
secure=False
|
||
)
|
||
|
||
# 使用缓存下载tileset
|
||
tileset_url = "http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/石棉0908/terra_b3dms/tileset.json"
|
||
|
||
# 第一次下载(会下载到本地)
|
||
success, result = manager.download_full_tileset(tileset_url, use_cache=True)
|
||
if success:
|
||
print(f"下载成功,本地路径: {result}")
|
||
|
||
# 第二次下载相同URL(直接从缓存返回)
|
||
success, result = manager.download_full_tileset(tileset_url, use_cache=True)
|
||
if success:
|
||
print(f"从缓存获取,本地路径: {result}")
|
||
|
||
# 强制重新下载(忽略缓存)
|
||
success, result = manager.download_full_tileset(tileset_url, use_cache=False)
|
||
if success:
|
||
print("强制重新下载成功")
|
||
|
||
# 获取缓存的本地路径
|
||
local_path = manager.get_tileset_local_path(tileset_url)
|
||
if local_path:
|
||
print(f"缓存的本地路径: {local_path}") |