Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.vscode

# Swift build artifacts that might be created in the wrong location
*.swiftdeps
*.o
*.d
*.swiftmodule
*.swiftsourceinfo
*.swiftdoc
6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ let package = Package(
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
.product(name: "EdgeAgentGRPC", package: "edge-agent-common"),
.target(name: "EdgeCLI"),
.target(name: "Imager"),
],
resources: [
.copy("Resources")
Expand All @@ -55,5 +56,10 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
]
),

/// Tools to put EdgeOS images onto drives.
.target(
name: "Imager"
),
]
)
35 changes: 35 additions & 0 deletions Sources/Imager/Drive.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
/// A structure representing a drive.
public struct Drive {
/// The path to the drive.
public var path: String
/// The total capacity of the drive in bytes.
public var capacity: Int64
/// The available free space on the drive in bytes, if known.
public var available: Int64?
/// The name of the drive.
public var name: String?

/// The total capacity of the drive in a human-readable format.
public var capacityHumanReadable: String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useAll]
formatter.countStyle = .file
formatter.includesUnit = true
formatter.isAdaptive = true
return formatter.string(fromByteCount: capacity)
}

/// The available space on the drive in a human-readable format, or "N/A" if unknown.
public var availableHumanReadable: String {
guard let availableSpace = available else {
return "N/A"
}
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useAll]
formatter.countStyle = .file
formatter.includesUnit = true
formatter.isAdaptive = true
return formatter.string(fromByteCount: availableSpace)
}
}
102 changes: 102 additions & 0 deletions Sources/Imager/ImagerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/// Error types that can occur during the imaging process.
///
/// These errors represent various failure scenarios that might occur when
/// attempting to write an image file to a drive.
public enum ImagerError: Error, Sendable {
/// The provided image file is invalid.
///
/// This error occurs when the image file cannot be read or is in an unsupported format.
/// Only ZIP, TAR, and IMG formats are supported.
case invalidImageFile(reason: String)

/// The selected drive is invalid.
///
/// This error occurs when the specified drive cannot be used as a target.
/// The associated value provides the operating system's specific reason for the failure.
case invalidDrive(reason: String)

/// Permission to access the drive was denied.
///
/// This error occurs when the application does not have sufficient privileges
/// to write to the selected drive. The associated value provides the operating
/// system's specific reason for the permission denial.
case permissionDenied(reason: String)

/// The image file is too large for the selected drive.
///
/// This error occurs when the size of the image file exceeds the available
/// space on the target drive.
case imageTooLargeForDrive(imageSize: UInt64, driveSize: UInt64)

/// The imaging process was interrupted.
///
/// This error occurs when the imaging process is unexpectedly terminated
/// before completion. The associated value provides information about the
/// cause of the interruption.
case processingInterrupted(reason: String)

/// Error detecting or enumerating drives.
///
/// This error occurs when there is an issue detecting or enumerating drives.
/// The associated value provides information about the cause of the error.
case driveDetectionError(reason: String)

/// Error related to disk watching functionality.
///
/// This error occurs when setting up or managing DiskArbitration callbacks fails.
case diskWatchError(reason: String)

/// Functionality not implemented.
///
/// This error occurs when a feature is not yet implemented for a particular platform.
case notImplemented(reason: String)

/// An unknown error occurred.
///
/// This error represents any failure that doesn't fall into the other categories.
/// It should be used as a last resort when the specific error type cannot be determined.
case unknown(reason: String?)

}

// MARK: - CustomStringConvertible

extension ImagerError: CustomStringConvertible {
/// A user-friendly description of the error.
public var description: String {
switch self {
case .invalidImageFile(let reason):
return "Invalid Image File: \(reason). Only ZIP, TAR, and IMG formats are supported."

case .invalidDrive(let reason):
return "Invalid drive: \(reason)"

case .permissionDenied(let reason):
return "Permission denied: \(reason)"

case .imageTooLargeForDrive(let imageSize, let driveSize):
let imageSizeMB = Double(imageSize) / 1_048_576
let driveSizeMB = Double(driveSize) / 1_048_576
return "Image too large for drive: Image size is \(String(format: "%.1f", imageSizeMB)) MB, but drive size is only \(String(format: "%.1f", driveSizeMB)) MB"

case .processingInterrupted(let reason):
return "Imaging process was interrupted: \(reason)"

case .driveDetectionError(let reason):
return "Error detecting drives: \(reason)"

case .diskWatchError(let reason):
return "Disk Watch Error: \(reason)"

case .notImplemented(let reason):
return "Functionality not implemented: \(reason)"

case .unknown(let reason):
if let reason = reason, !reason.isEmpty {
return "An unknown error occurred: \(reason)"
} else {
return "An unknown error occurred"
}
}
}
}
34 changes: 34 additions & 0 deletions Sources/Imager/ImagerFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

/// Provides factory methods for creating platform-specific Imager and DiskLister instances.
public struct ImagerFactory {

/// Creates a platform-specific object conforming to the Imager protocol.
/// - Parameters:
/// - imageFilePath: The path to the source image file.
/// - drivePath: The path to the target drive (e.g., "disk4").
/// - Returns: An object conforming to Imager.
public static func createImager(imageFilePath: String, drivePath: String) -> Imager {
#if os(macOS)
return MacOSImager(imageFilePath: imageFilePath, drivePath: drivePath)
#elseif os(Linux)
// return LinuxImager(imageFilePath: imageFilePath, drivePath: drivePath)
fatalError("Linux implementation not yet available.")
#else
fatalError("Unsupported operating system.")
#endif
}

/// Creates a platform-specific object conforming to the DiskLister protocol.
/// - Returns: An object conforming to DiskLister.
public static func createDiskLister() -> DiskLister {
#if os(macOS)
return MacOSDiskLister()
#elseif os(Linux)
// return LinuxDiskLister()
fatalError("Linux implementation not yet available.")
#else
fatalError("Unsupported operating system.")
#endif
}
}
52 changes: 52 additions & 0 deletions Sources/Imager/ImagerState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Represents the current status of the imaging process.
///
/// This enum tracks the lifecycle of an imaging operation, from initialization through
/// completion or failure.
public enum ImagerState: Sendable {
/// The imager is initialized but has not started imaging.
///
/// This is the default state before `startImaging()` is called.
case idle

/// The imaging process has started but has not yet completed or failed.
///
/// This state indicates that the operation is active.
case imaging

/// The imaging process is being cancelled.
case cancelling

/// The imaging process completed successfully.
///
/// This state indicates that the image file was written to the drive without errors.
case completed

/// The imaging process failed.
///
/// The associated value `ImagerError` provides details about the failure reason.
case failed(ImagerError)
}

// MARK: - CustomStringConvertible

extension ImagerState: CustomStringConvertible {
/// A user-friendly description of the current imaging state.
public var description: String {
switch self {
case .idle:
return "Ready to begin imaging"

case .imaging:
return "Imaging in progress"

case .cancelling:
return "Cancelling imaging process"

case .completed:
return "Imaging completed successfully"

case .failed(let error):
return "Imaging failed: \(error.localizedDescription)"
}
}
}
Loading
Loading