"""RustFS 对象存储封装(基于 boto3)""" import os import hashlib import urllib.parse from pathlib import Path import boto3 import botocore from db.database import load_config def _s3(): cfg = load_config()['rustfs'] return boto3.client( 's3', endpoint_url=cfg['endpoint'], aws_access_key_id=cfg['access_key'], aws_secret_access_key=cfg['secret_key'], region_name='us-east-1', config=botocore.config.Config( signature_version='s3v4', s3={'addressing_style': 'path'}, ), verify=cfg.get('verify_ssl', False), ) def _content_type(path: str) -> str: p = path.lower() if p.endswith('.html'): return 'text/html; charset=utf-8' if p.endswith('.js'): return 'application/javascript' if p.endswith('.json'): return 'application/json' if p.endswith('.docx'): return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' if p.endswith('.png'): return 'image/png' if p.endswith('.log'): return 'text/plain; charset=utf-8' if p.endswith('.zip'): return 'application/zip' return 'application/octet-stream' def upload(local_path: str, key: str, bucket: str | None = None) -> dict: """上传文件 → 返回 {url, key, bucket, size, checksum}""" cfg = load_config()['rustfs'] bucket = bucket or cfg['default_bucket'] size = os.path.getsize(local_path) # sha256 h = hashlib.sha256() with open(local_path, 'rb') as f: for chunk in iter(lambda: f.read(65536), b''): h.update(chunk) checksum = h.hexdigest() _s3().upload_file( local_path, bucket, key, ExtraArgs={'ContentType': _content_type(local_path)}, ) url = f"{cfg['public_base']}/{bucket}/" + urllib.parse.quote(key, safe='/') return { 'url': url, 'key': key, 'bucket': bucket, 'size': size, 'checksum': checksum, } def delete_prefix(prefix: str, bucket: str | None = None) -> int: """批量删一个前缀下的所有对象,返回删除条数""" cfg = load_config()['rustfs'] bucket = bucket or cfg['default_bucket'] s3 = _s3() n = 0 paginator = s3.get_paginator('list_objects_v2') for page in paginator.paginate(Bucket=bucket, Prefix=prefix): keys = [{'Key': o['Key']} for o in page.get('Contents', [])] if keys: s3.delete_objects(Bucket=bucket, Delete={'Objects': keys}) n += len(keys) return n def build_key(book_id: int, doc_id: str, file_type: str) -> str: """根据 book_id / doc_id / file_type 生成对象存储 key""" cfg = load_config()['storage'] file_naming = cfg['file_naming'] filename = file_naming.get(file_type, f'{file_type}.bin') return f"{cfg['book_prefix']}/{book_id}/{cfg['docs_subdir']}/{doc_id}/{filename}"