mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-21 15:07:54 -04:00
412 lines
16 KiB
Swift
412 lines
16 KiB
Swift
#if os(iOS)
|
|
import Foundation
|
|
import UIKit
|
|
|
|
/// Protocol for the iOS core bridge - allows decoupling from specific implementation
|
|
public protocol IOSCoreBridge {
|
|
func initialize(dataDirectory: String, deviceName: String?) -> Bool
|
|
func sendMessage(_ query: String, dataDirectory: String) async throws -> String
|
|
func startEventListener(handler: @escaping (String) -> Void)
|
|
func shutdown()
|
|
}
|
|
|
|
/// Main client for interacting with the embedded Spacedrive core
|
|
///
|
|
/// This client provides a clean, type-safe interface for executing queries,
|
|
/// actions, and subscribing to events from the Spacedrive core running embedded within the iOS app.
|
|
public class SpacedriveClient {
|
|
private let embeddedCore: IOSCoreBridge
|
|
private let dataDirectory: String
|
|
|
|
/// The currently active library ID
|
|
/// This is used for library-scoped operations
|
|
private var currentLibraryId: String?
|
|
|
|
/// Thread-safe access to current library ID
|
|
private let libraryIdQueue = DispatchQueue(
|
|
label: "com.spacedrive.library-id", attributes: .concurrent)
|
|
|
|
/// Initialize a new embedded Spacedrive client
|
|
/// - Parameters:
|
|
/// - core: The iOS core bridge implementation
|
|
/// - dataDirectory: Path to the data directory for the embedded core
|
|
/// - deviceName: Optional device name (defaults to UIDevice.current.name on iOS)
|
|
public init(core: IOSCoreBridge, dataDirectory: String, deviceName: String? = nil)
|
|
async throws
|
|
{
|
|
self.embeddedCore = core
|
|
self.dataDirectory = dataDirectory
|
|
|
|
// Get device name from UIDevice if not provided
|
|
#if os(iOS)
|
|
let finalDeviceName = deviceName ?? UIDevice.current.name
|
|
#else
|
|
let finalDeviceName = deviceName
|
|
#endif
|
|
|
|
guard embeddedCore.initialize(dataDirectory: dataDirectory, deviceName: finalDeviceName)
|
|
else {
|
|
throw SpacedriveError.connectionFailed("Failed to initialize embedded core")
|
|
}
|
|
}
|
|
|
|
// MARK: - API Namespaces
|
|
|
|
/// Core API operations (device management, network, etc.)
|
|
public lazy var core = CoreAPI(client: self)
|
|
|
|
/// Device management operations
|
|
public lazy var devices = DevicesAPI(client: self)
|
|
|
|
/// Library management operations
|
|
public lazy var libraries = LibrariesAPI(client: self)
|
|
|
|
/// Job management operations
|
|
public lazy var jobs = JobsAPI(client: self)
|
|
|
|
/// Location management operations
|
|
public lazy var locations = LocationsAPI(client: self)
|
|
|
|
/// Media operations
|
|
public lazy var media = MediaAPI(client: self)
|
|
|
|
/// Network operations
|
|
public lazy var network = NetworkAPI(client: self)
|
|
|
|
/// Search operations
|
|
public lazy var search = SearchAPI(client: self)
|
|
|
|
/// Tag operations
|
|
public lazy var tags = TagsAPI(client: self)
|
|
|
|
/// Volume operations
|
|
public lazy var volumes = VolumesAPI(client: self)
|
|
|
|
/// File operations
|
|
public lazy var files = FilesAPI(client: self)
|
|
|
|
// MARK: - Library Management
|
|
|
|
/// Get the currently active library ID
|
|
/// - Returns: The current library ID, or nil if no library is active
|
|
public func getCurrentLibraryId() -> String? {
|
|
return libraryIdQueue.sync { currentLibraryId }
|
|
}
|
|
|
|
/// Set the currently active library
|
|
/// - Parameter libraryId: The ID of the library to make active
|
|
public func setCurrentLibrary(_ libraryId: String) {
|
|
libraryIdQueue.async(flags: .barrier) {
|
|
self.currentLibraryId = libraryId
|
|
}
|
|
}
|
|
|
|
/// Clear the currently active library (set to nil)
|
|
public func clearCurrentLibrary() {
|
|
libraryIdQueue.async(flags: .barrier) {
|
|
self.currentLibraryId = nil
|
|
}
|
|
}
|
|
|
|
/// Switch to a library by ID
|
|
/// - Parameter libraryId: The ID of the library to switch to
|
|
/// - Throws: SpacedriveError if the library doesn't exist or can't be accessed
|
|
public func switchToLibrary(_ libraryId: String) async throws {
|
|
// Check if the library exists in the list (core-scoped query)
|
|
let libraries = try await getLibraries()
|
|
let libraryExists = libraries.contains { $0.id == libraryId }
|
|
|
|
if !libraryExists {
|
|
throw SpacedriveError.invalidResponse("Library with ID '\(libraryId)' not found")
|
|
}
|
|
|
|
// Set as current library
|
|
setCurrentLibrary(libraryId)
|
|
}
|
|
|
|
/// Switch to a library by name
|
|
/// - Parameter libraryName: The name of the library to switch to
|
|
/// - Throws: SpacedriveError if the library doesn't exist or multiple libraries have the same name
|
|
public func switchToLibrary(named libraryName: String) async throws {
|
|
let libraries = try await getLibraries()
|
|
let matchingLibraries = libraries.filter { $0.name == libraryName }
|
|
|
|
switch matchingLibraries.count {
|
|
case 0:
|
|
throw SpacedriveError.invalidResponse("No library found with name '\(libraryName)'")
|
|
case 1:
|
|
setCurrentLibrary(matchingLibraries[0].id)
|
|
default:
|
|
throw SpacedriveError.invalidResponse(
|
|
"Multiple libraries found with name '\(libraryName)'. Use switchToLibrary(id:) instead."
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get information about the currently active library
|
|
/// - Returns: LibraryInfo for the current library, or nil if no library is active
|
|
/// - Throws: SpacedriveError if the library can't be accessed
|
|
public func getCurrentLibraryInfo() async throws -> LibraryInfo? {
|
|
guard let libraryId = getCurrentLibraryId() else {
|
|
return nil
|
|
}
|
|
|
|
let libraries = try await getLibraries()
|
|
return libraries.first { $0.id == libraryId }
|
|
}
|
|
|
|
/// Check if a library operation can be performed (requires current library)
|
|
/// - Throws: SpacedriveError if no library is currently active
|
|
private func requireCurrentLibrary() throws {
|
|
guard getCurrentLibraryId() != nil else {
|
|
throw SpacedriveError.invalidResponse(
|
|
"This operation requires an active library. Use switchToLibrary() or createAndSwitchToLibrary() first."
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get the current library ID or throw an error if none is set
|
|
/// - Returns: The current library ID
|
|
/// - Throws: SpacedriveError if no library is currently active
|
|
private func getCurrentLibraryIdOrThrow() throws -> String {
|
|
guard let libraryId = getCurrentLibraryId() else {
|
|
throw SpacedriveError.invalidResponse(
|
|
"This operation requires an active library. Use switchToLibrary() or createAndSwitchToLibrary() first."
|
|
)
|
|
}
|
|
return libraryId
|
|
}
|
|
|
|
// MARK: - Core API Methods
|
|
|
|
/// Internal method to execute both queries and actions via JSON-RPC over FFI
|
|
/// - Parameters:
|
|
/// - requestPayload: The input payload (can be empty struct for parameterless operations)
|
|
/// - method: The method identifier (e.g., "query:core.status" or "action:libraries.create.input")
|
|
/// - responseType: The expected response type
|
|
/// - libraryId: Optional library ID to override the current library (for library-scoped operations)
|
|
/// - Returns: The operation result
|
|
internal func execute<Request: Codable, Response: Codable>(
|
|
_ requestPayload: Request,
|
|
method: String,
|
|
responseType: Response.Type,
|
|
libraryId: String? = nil
|
|
) async throws -> Response {
|
|
// Determine effective library ID
|
|
let effectiveLibraryId: String?
|
|
if method.hasPrefix("query:core.") || method.hasPrefix("action:core.") {
|
|
// Core operations don't use library ID
|
|
effectiveLibraryId = nil
|
|
} else {
|
|
effectiveLibraryId = libraryId ?? getCurrentLibraryId()
|
|
}
|
|
|
|
// Build JSON-RPC request
|
|
let jsonRpc = try JSONRPCRequest(
|
|
method: method,
|
|
input: requestPayload,
|
|
library_id: effectiveLibraryId,
|
|
id: UUID().uuidString
|
|
)
|
|
|
|
// Send via FFI
|
|
do {
|
|
let requestData = try JSONEncoder().encode(jsonRpc)
|
|
let requestJson = String(data: requestData, encoding: .utf8)!
|
|
|
|
let responseJson = try await embeddedCore.sendMessage(
|
|
requestJson, dataDirectory: dataDirectory)
|
|
let responseData = responseJson.data(using: String.Encoding.utf8)!
|
|
|
|
// Parse JSON-RPC response
|
|
let jsonRpcResponse = try JSONDecoder().decode(
|
|
JSONRPCResponse<Response>.self, from: responseData)
|
|
|
|
if let result = jsonRpcResponse.result {
|
|
return result
|
|
} else if let error = jsonRpcResponse.error {
|
|
throw SpacedriveError.daemonError(error.message)
|
|
} else {
|
|
throw SpacedriveError.invalidResponse("No result or error in JSON-RPC response")
|
|
}
|
|
} catch let error as SpacedriveError {
|
|
throw error
|
|
} catch {
|
|
throw SpacedriveError.serializationError("Failed to execute request: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Subscribe to events from the embedded core
|
|
/// - Parameter eventTypes: Array of event type names to subscribe to
|
|
/// - Returns: An async stream of events
|
|
public func subscribe(
|
|
to eventTypes: [String] = []
|
|
) -> AsyncThrowingStream<Event, Error> {
|
|
AsyncThrowingStream { continuation in
|
|
Task {
|
|
do {
|
|
// Start the FFI-based event listener
|
|
embeddedCore.startEventListener { [weak self] (eventJson: String) in
|
|
guard self != nil else { return }
|
|
|
|
do {
|
|
let eventData = eventJson.data(using: String.Encoding.utf8)!
|
|
let event = try JSONDecoder().decode(Event.self, from: eventData)
|
|
|
|
// Filter events if specific types were requested
|
|
if eventTypes.isEmpty
|
|
|| self?.shouldIncludeEvent(event, in: eventTypes) == true
|
|
{
|
|
continuation.yield(event)
|
|
}
|
|
} catch {
|
|
continuation.finish(
|
|
throwing: SpacedriveError.serializationError(
|
|
"Failed to decode event: \(error)"))
|
|
}
|
|
}
|
|
} catch {
|
|
continuation.finish(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if an event should be included based on the requested event types
|
|
private func shouldIncludeEvent(_ event: Event, in eventTypes: [String]) -> Bool {
|
|
// This is a simplified implementation - in practice you'd want to map
|
|
// the Event enum cases to string identifiers and check against eventTypes
|
|
return true
|
|
}
|
|
|
|
// MARK: - Convenience Methods (same as macOS implementation)
|
|
|
|
/// Create a library using generated types
|
|
public func createLibrary(name: String, path: String? = nil) async throws
|
|
-> LibraryCreateOutput
|
|
{
|
|
let input = LibraryCreateInput(name: name, path: path)
|
|
|
|
return try await execute(
|
|
input,
|
|
method: "action:libraries.create.input",
|
|
responseType: LibraryCreateOutput.self
|
|
)
|
|
}
|
|
|
|
/// Create a library and automatically set it as the current library
|
|
/// - Parameters:
|
|
/// - name: The name of the library to create
|
|
/// - path: Optional path for the library
|
|
/// - setAsCurrent: Whether to automatically set the new library as current (default: true)
|
|
/// - Returns: The created library information
|
|
/// - Throws: SpacedriveError if creation fails
|
|
public func createAndSwitchToLibrary(
|
|
name: String, path: String? = nil, setAsCurrent: Bool = true
|
|
) async throws -> LibraryCreateOutput {
|
|
let result = try await createLibrary(name: name, path: path)
|
|
|
|
if setAsCurrent {
|
|
setCurrentLibrary(result.libraryId)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/// Get list of libraries using generated types
|
|
public func getLibraries(includeStats: Bool = false) async throws -> [LibraryInfo] {
|
|
struct LibraryListQuery: Codable {
|
|
let include_stats: Bool
|
|
}
|
|
|
|
let query = LibraryListQuery(include_stats: includeStats)
|
|
|
|
return try await execute(
|
|
query,
|
|
method: "query:libraries.list",
|
|
responseType: [LibraryInfo].self
|
|
)
|
|
}
|
|
|
|
/// Get list of jobs using generated types
|
|
public func getJobs(status: JobStatus? = nil) async throws -> JobListOutput {
|
|
struct JobListQuery: Codable {
|
|
let status: String?
|
|
}
|
|
|
|
let query = JobListQuery(status: nil) // TODO: Convert JobStatus to string
|
|
|
|
return try await execute(
|
|
query,
|
|
method: "query:jobs.list",
|
|
responseType: JobListOutput.self
|
|
)
|
|
}
|
|
|
|
/// Get jobs for the current library
|
|
/// - Parameter status: Optional job status filter
|
|
/// - Returns: List of jobs for the current library
|
|
/// - Throws: SpacedriveError if no library is active or operation fails
|
|
public func getCurrentLibraryJobs(status: JobStatus? = nil) async throws -> JobListOutput {
|
|
let libraryId = try getCurrentLibraryIdOrThrow()
|
|
|
|
struct JobListQuery: Codable {
|
|
let status: String?
|
|
}
|
|
|
|
let query = JobListQuery(status: nil) // TODO: Convert JobStatus to string
|
|
|
|
return try await execute(
|
|
query,
|
|
method: "query:jobs.list",
|
|
responseType: JobListOutput.self,
|
|
libraryId: libraryId
|
|
)
|
|
}
|
|
|
|
/// Get the current library status
|
|
/// - Returns: A string describing the current library state
|
|
public func getCurrentLibraryStatus() -> String {
|
|
guard let libraryId = getCurrentLibraryId() else {
|
|
return "No library is currently active"
|
|
}
|
|
return "Library '\(libraryId)' is currently active"
|
|
}
|
|
|
|
/// Check if a library is currently active
|
|
/// - Returns: True if a library is active, false otherwise
|
|
public func hasActiveLibrary() -> Bool {
|
|
return getCurrentLibraryId() != nil
|
|
}
|
|
|
|
/// Ping the embedded core to test connectivity
|
|
public func ping() async throws {
|
|
print("Testing embedded core connectivity...")
|
|
|
|
// Send a simple core status query as a ping
|
|
do {
|
|
let emptyInput = Empty()
|
|
_ = try await execute(
|
|
emptyInput,
|
|
method: "query:core.status",
|
|
responseType: CoreStatus.self
|
|
)
|
|
print("Embedded core ping successful!")
|
|
} catch {
|
|
print("Embedded core ping failed: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/// Shutdown the embedded core
|
|
public func shutdown() {
|
|
embeddedCore.shutdown()
|
|
}
|
|
|
|
deinit {
|
|
shutdown()
|
|
}
|
|
}
|
|
|
|
#endif
|