220 lines
5.7 KiB
Markdown
220 lines
5.7 KiB
Markdown
---
|
||
title: RustFS简介和使用
|
||
date: 2025-12-09
|
||
---
|
||
|
||
# 简介
|
||
  [RustFS](https://rustfs.com.cn/) 是一个基于 Rust 构建的高性能分布式对象存储系统。
|
||
  具体以下特点:
|
||
|
||
- 高性能:基于 Rust 构建,确保极致的速度和资源效率。
|
||
- 分布式架构:可扩展且容错的设计,适用于大规模部署。
|
||
- S3 兼容性:与现有的 S3 兼容应用和工具无缝集成。
|
||
- 数据湖支持:专为高吞吐量的大数据和 AI 工作负载优化。
|
||
- 完全开源:采用 Apache 2.0 许可证,鼓励社区贡献和商业使用。
|
||
- 简单易用:设计简洁,易于部署和管理。
|
||
|
||
  S3 标准通常指 Amazon S3 的 API 接口规范,现已成为对象存储领域事实上的行业标准接口协议。它定义了对象存储的核心操作方式和数据模型。
|
||
- 对象:基本存储单元,包含数据、元数据和唯一标识符
|
||
- 桶:对象的逻辑容器,类似文件夹
|
||
- 扁平结构:无传统目录层级,通过键名(Key)定位
|
||
|
||
# 安装部署
|
||
  通过docker部署:
|
||
```yml
|
||
services:
|
||
rustfs:
|
||
image: rustfs/rustfs:1.0.0-alpha.65
|
||
container_name: rustfs
|
||
ports:
|
||
- "9000:9000"
|
||
- "9001:9001"
|
||
volumes:
|
||
- ./data:/data
|
||
- ./logs:/logs
|
||
environment:
|
||
- TZ=Asia/Shanghai
|
||
- RUSTFS_ACCESS_KEY=admin
|
||
- RUSTFS_SECRET_KEY=19940822Cxx
|
||
- RUSTFS_CONSOLE_ENABLE=true
|
||
restart: unless-stopped
|
||
```
|
||
|
||
  其中9000是API端口,9001是控制台端口。
|
||
  默认的登录账号密码为:rustfsadmin / rustfsadmin。
|
||
|
||
# API调用
|
||
  登录控制台,获取access_key和secret_access。
|
||
|
||
## Python
|
||
1. 安装AWS官方SDK依赖库
|
||
```cmd
|
||
pip install boto3
|
||
```
|
||
|
||
2. 配置与连接
|
||
```python
|
||
import boto3
|
||
from botocore.client import Config
|
||
|
||
from config.setting import settings
|
||
|
||
url = 'ip:port'
|
||
access_key = ''
|
||
secret_access = ''
|
||
|
||
s3 = boto3.client('s3',
|
||
endpoint_url=url,
|
||
aws_access_key_id=access_key,
|
||
aws_secret_access_key=secret_access,
|
||
config=Config(signature_version='s3v4'),
|
||
region_name='cn-east-1'
|
||
)
|
||
```
|
||
|
||
3. 上传文件
|
||
```python
|
||
async def upload_file(md5: str, file: UploadFile = File(...)) -> str:
|
||
file_ext = file.filename.split('.')[-1]
|
||
unique_filename = f"{md5}.{file_ext}"
|
||
|
||
file_content = await file.read()
|
||
|
||
# 上传到S3
|
||
s3.put_object(
|
||
Bucket=BUCKET,
|
||
Key=unique_filename,
|
||
Body=file_content,
|
||
ContentType=file.content_type
|
||
)
|
||
|
||
# 返回文件url
|
||
return f"{BUCKET}/{unique_filename}"
|
||
```
|
||
|
||
## Flutter
|
||
1. 安装依赖包
|
||
```yaml
|
||
dependencies:
|
||
minio: ^3.5.8
|
||
file_picker: ^10.3.3
|
||
```
|
||
|
||
2. 工具类
|
||
```dart
|
||
import 'dart:io';
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:minio/io.dart';
|
||
import 'package:minio/minio.dart';
|
||
|
||
import 'file_utils.dart';
|
||
|
||
class MinIOHelper {
|
||
static final MinIOHelper _instance = MinIOHelper._internal();
|
||
|
||
factory MinIOHelper() => _instance;
|
||
|
||
final String ip = 'ip';
|
||
final int port = 9100;
|
||
final String fileUrl = 'http://ip:port';
|
||
final String accessKey = '';
|
||
final String secretKey = '';
|
||
|
||
MinIOHelper._internal() {
|
||
_minio = Minio(
|
||
endPoint: ip,
|
||
port: port,
|
||
accessKey: accessKey,
|
||
secretKey: secretKey,
|
||
useSSL: false,
|
||
);
|
||
}
|
||
|
||
late Minio _minio;
|
||
|
||
Future<String> uploadFile({
|
||
required PlatformFile file,
|
||
required String bucketName,
|
||
Function(double)? onProgress,
|
||
}) async {
|
||
try {
|
||
if (isImageFile(file.name)) {
|
||
// 压缩图片
|
||
final compressedFile = await compressImage(File(file.path!));
|
||
|
||
String hashName = await generateMD5HashName(compressedFile.path);
|
||
String fileName = '$hashName${getFileExtension(file.name)}';
|
||
|
||
await _minio.fPutObject(bucketName, fileName, compressedFile.path);
|
||
await compressedFile.delete();
|
||
|
||
return fileName;
|
||
} else {
|
||
String hashName = await generateMD5HashName(file.path!);
|
||
String fileName = '$hashName${getFileExtension(file.name)}';
|
||
|
||
await _minio.fPutObject(bucketName, fileName, file.path!);
|
||
|
||
return fileName;
|
||
}
|
||
} catch (e) {
|
||
throw Exception('文件上传失败: $e');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
  文件工具类:
|
||
```dart
|
||
import 'dart:io';
|
||
import 'package:crypto/crypto.dart';
|
||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||
|
||
import 'log_utils.dart';
|
||
|
||
String getFileExtension(String fileName) {
|
||
if (fileName.contains('.')) {
|
||
return '.${fileName.split('.').last.toLowerCase()}';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
Future<String> generateMD5HashName(String filePath) async {
|
||
final file = File(filePath);
|
||
final bytes = await file.readAsBytes();
|
||
final hash = md5.convert(bytes);
|
||
return hash.toString();
|
||
}
|
||
|
||
// 图片压缩方法
|
||
Future<File> compressImage(File file) async {
|
||
try {
|
||
// 获取压缩后的文件路径
|
||
final result = await FlutterImageCompress.compressAndGetFile(
|
||
file.absolute.path,
|
||
'${file.parent.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||
minWidth: 800,
|
||
minHeight: 600,
|
||
quality: 70,
|
||
format: CompressFormat.jpeg,
|
||
);
|
||
|
||
if (result == null) {
|
||
throw Exception('图片压缩失败');
|
||
}
|
||
|
||
return File(result.path);
|
||
} catch (e) {
|
||
logger.e('图片压缩失败,使用原文件: $e');
|
||
return file;
|
||
}
|
||
}
|
||
|
||
// 检查是否为图片文件
|
||
bool isImageFile(String fileName) {
|
||
final imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
|
||
final extension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
|
||
return imageExtensions.contains(extension);
|
||
}
|
||
``` |