核心功能: - ✅ Categories/Series双视图管理(category_view.rs + import_markdown.rs) - ✅ FUSE Multi-Volume支持(tree_type参数) - ✅ SSH/SFTP/SCP/rsync协议完整实现(4042行) - ✅ NFS/SMB Module Phase 1-3完成 - ✅ Archive Module Phase 1-4完成(2916行) - ✅ Download Center API完整实现 - ✅ S3兼容API实现(560行) Git配置修正: - ✅ 删除错误origin(gitea.momentry.ddns.net) - ✅ 删除m5max128(指向机器名) - ✅ 设置origin = m5max128gitea.momentry.ddns.net/admin/markbase - ✅ 设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase 数据清理: - ✅ 删除38个临时SQLite(保留accusys.sqlite、demo.sqlite) - ✅ 删除.bak、test_*.bin、调试脚本等临时文件 - ✅ 删除临时目录(build/、download files/、raid_test/等) - ✅ 更新.gitignore排除临时文件 架构优化: - 52个文件修改,2434行新增,4739行删除 - Workspace成员整合(16个crate) - 数据库状态:accusys.sqlite保留(主demo测试) 远程同步: - ✅ 准备推送到m5max128gitea(远程Gitea) - ✅ 准备推送到m4minigitea(本地Gitea)
358 lines
12 KiB
Swift
358 lines
12 KiB
Swift
import Foundation
|
|
|
|
public class ObjectStorageClient {
|
|
|
|
// Object Storage HTTP API Client
|
|
// Supports S3, MinIO, Ceph RADOS Gateway
|
|
// No DriverKit Entitlement required
|
|
|
|
private let endpoint: String
|
|
private let accessKey: String
|
|
private let secretKey: String
|
|
private let session: URLSession
|
|
|
|
public init(endpoint: String, accessKey: String, secretKey: String) {
|
|
self.endpoint = endpoint
|
|
self.accessKey = accessKey
|
|
self.secretKey = secretKey
|
|
self.session = URLSession.shared
|
|
|
|
print("ObjectStorageClient initializing...")
|
|
print(" - Endpoint: \(endpoint)")
|
|
print(" - Access Key: \(accessKey)")
|
|
}
|
|
|
|
// MARK: - Bucket Operations
|
|
|
|
public func createBucket(bucketName: String) -> Bool {
|
|
print("Creating bucket: \(bucketName)")
|
|
|
|
let url = URL(string: "\(endpoint)/\(bucketName)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "PUT"
|
|
|
|
// Add authentication headers
|
|
addAuthHeaders(request: &request, method: "PUT", path: "/\(bucketName)")
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Error creating bucket: \(error)")
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 200 || httpResponse.statusCode == 204 {
|
|
print("Bucket created successfully: \(bucketName)")
|
|
} else {
|
|
print("Failed to create bucket: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
}
|
|
|
|
task.resume()
|
|
|
|
return true
|
|
}
|
|
|
|
public func listBuckets() -> [String] {
|
|
print("Listing buckets...")
|
|
|
|
var buckets: [String] = []
|
|
|
|
let url = URL(string: "\(endpoint)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
|
|
addAuthHeaders(request: &request, method: "GET", path: "/")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Error listing buckets: \(error)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 200 {
|
|
print("Buckets listed successfully")
|
|
|
|
// Parse XML response (simplified)
|
|
if let data = data {
|
|
let xmlString = String(data: data, encoding: .utf8)
|
|
print("Response: \(xmlString ?? "")")
|
|
}
|
|
} else {
|
|
print("Failed to list buckets: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
return buckets
|
|
}
|
|
|
|
// MARK: - Object Operations
|
|
|
|
public func uploadObject(bucket: String, key: String, data: Data) -> Bool {
|
|
print("Uploading object: \(key) to bucket: \(bucket)")
|
|
|
|
let url = URL(string: "\(endpoint)/\(bucket)/\(key)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "PUT"
|
|
request.httpBody = data
|
|
|
|
addAuthHeaders(request: &request, method: "PUT", path: "/\(bucket)/\(key)")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var success = false
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Error uploading object: \(error)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 200 {
|
|
print("Object uploaded successfully: \(key)")
|
|
success = true
|
|
} else {
|
|
print("Failed to upload object: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
return success
|
|
}
|
|
|
|
public func downloadObject(bucket: String, key: String) -> Data? {
|
|
print("Downloading object: \(key) from bucket: \(bucket)")
|
|
|
|
let url = URL(string: "\(endpoint)/\(bucket)/\(key)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
|
|
addAuthHeaders(request: &request, method: "GET", path: "/\(bucket)/\(key)")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var downloadedData: Data? = nil
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Error downloading object: \(error)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 200 {
|
|
print("Object downloaded successfully: \(key)")
|
|
downloadedData = data
|
|
} else {
|
|
print("Failed to download object: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
return downloadedData
|
|
}
|
|
|
|
public func deleteObject(bucket: String, key: String) -> Bool {
|
|
print("Deleting object: \(key) from bucket: \(bucket)")
|
|
|
|
let url = URL(string: "\(endpoint)/\(bucket)/\(key)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "DELETE"
|
|
|
|
addAuthHeaders(request: &request, method: "DELETE", path: "/\(bucket)/\(key)")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var success = false
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Error deleting object: \(error)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 204 || httpResponse.statusCode == 200 {
|
|
print("Object deleted successfully: \(key)")
|
|
success = true
|
|
} else {
|
|
print("Failed to delete object: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
return success
|
|
}
|
|
|
|
// MARK: - Authentication
|
|
|
|
private func addAuthHeaders(request: inout URLRequest, method: String, path: String) {
|
|
// Simplified AWS Signature v4 authentication
|
|
// For POC, using basic headers
|
|
|
|
let timestamp = ISO8601DateFormatter().string(from: Date())
|
|
|
|
request.addValue(accessKey, forHTTPHeaderField: "X-Amz-Access-Key")
|
|
request.addValue(timestamp, forHTTPHeaderField: "X-Amz-Date")
|
|
request.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
|
|
|
// Full AWS Signature v4 implementation would require:
|
|
// 1. Create canonical request
|
|
// 2. Create string to sign
|
|
// 3. Calculate signature
|
|
// 4. Add Authorization header
|
|
|
|
// For POC, simplified headers are sufficient
|
|
}
|
|
|
|
// MARK: - Test Operations
|
|
|
|
public func testConnection() -> Bool {
|
|
print("Testing Object Storage connection...")
|
|
|
|
let url = URL(string: "\(endpoint)")!
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var connected = false
|
|
|
|
let task = session.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
print("Connection test failed: \(error)")
|
|
semaphore.signal()
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode == 200 || httpResponse.statusCode == 403 {
|
|
// 403 is acceptable - means endpoint exists but auth required
|
|
print("Connection test successful: HTTP \(httpResponse.statusCode)")
|
|
connected = true
|
|
} else {
|
|
print("Connection test failed: HTTP \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
|
|
semaphore.signal()
|
|
}
|
|
|
|
task.resume()
|
|
semaphore.wait()
|
|
|
|
return connected
|
|
}
|
|
|
|
public func testObjectOperations() {
|
|
print("\n=== Object Storage Operations Test ===")
|
|
|
|
// Test 1: Connection test
|
|
print("Test 1: Connection Test")
|
|
let connected = testConnection()
|
|
|
|
if !connected {
|
|
print(" Result: ❌ FAILED - Object Storage service not available")
|
|
print(" Note: Skipping Object Storage tests")
|
|
print(" To enable Object Storage tests, start MinIO or S3-compatible service")
|
|
print("\n=== Object Storage Operations Test Skipped ===")
|
|
return
|
|
}
|
|
|
|
print(" Result: ✅ SUCCESS - Object Storage service available")
|
|
|
|
// Test 2: Create bucket
|
|
print("\nTest 2: Create Bucket")
|
|
let bucketCreated = createBucket(bucketName: "markbase-test-bucket")
|
|
print(" Result: \(bucketCreated ? "✅ SUCCESS" : "❌ FAILED")")
|
|
|
|
// Test 3: Upload object
|
|
print("\nTest 3: Upload Object")
|
|
let testData = "MarkBaseFS Object Storage Test".data(using: .utf8)!
|
|
let uploaded = uploadObject(bucket: "markbase-test-bucket", key: "test.txt", data: testData)
|
|
print(" Result: \(uploaded ? "✅ SUCCESS" : "❌ FAILED")")
|
|
|
|
// Test 4: Download object
|
|
print("\nTest 4: Download Object")
|
|
let downloadedData = downloadObject(bucket: "markbase-test-bucket", key: "test.txt")
|
|
if downloadedData != nil {
|
|
print(" Result: ✅ SUCCESS")
|
|
print(" Data size: \(downloadedData!.count) bytes")
|
|
} else {
|
|
print(" Result: ❌ FAILED")
|
|
}
|
|
|
|
// Test 5: Delete object
|
|
print("\nTest 5: Delete Object")
|
|
let deleted = deleteObject(bucket: "markbase-test-bucket", key: "test.txt")
|
|
print(" Result: \(deleted ? "✅ SUCCESS" : "❌ FAILED")")
|
|
|
|
print("\n=== Object Storage Operations Test Complete ===")
|
|
}
|
|
}
|
|
|
|
// MARK: - Object Storage Configuration
|
|
|
|
public struct ObjectStorageConfig {
|
|
let endpoint: String
|
|
let accessKey: String
|
|
let secretKey: String
|
|
let region: String
|
|
|
|
public init(endpoint: String, accessKey: String, secretKey: String, region: String = "us-east-1") {
|
|
self.endpoint = endpoint
|
|
self.accessKey = accessKey
|
|
self.secretKey = secretKey
|
|
self.region = region
|
|
}
|
|
|
|
public static func minIODefault() -> ObjectStorageConfig {
|
|
return ObjectStorageConfig(
|
|
endpoint: "http://localhost:9000",
|
|
accessKey: "minio_access_key",
|
|
secretKey: "minio_secret_key"
|
|
)
|
|
}
|
|
|
|
public static func s3Default() -> ObjectStorageConfig {
|
|
return ObjectStorageConfig(
|
|
endpoint: "https://s3.amazonaws.com",
|
|
accessKey: "aws_access_key",
|
|
secretKey: "aws_secret_key"
|
|
)
|
|
}
|
|
|
|
public static func cephDefault() -> ObjectStorageConfig {
|
|
return ObjectStorageConfig(
|
|
endpoint: "http://localhost:7480",
|
|
accessKey: "ceph_access_key",
|
|
secretKey: "ceph_secret_key"
|
|
)
|
|
}
|
|
} |