world-manager/World Manager for Minecraft/Services/WorldExporter.swift

98 lines
3.3 KiB
Swift

//
// WorldExporter.swift
// World Manager for Minecraft
//
// Created by John Burwell on 2026-05-25.
//
import Foundation
enum WorldExporter {
enum ExportError: LocalizedError {
case unsupportedContentType
case failedToCreateArchive(String)
var errorDescription: String? {
switch self {
case .unsupportedContentType:
return "Only Minecraft worlds can be exported as .mcworld files."
case .failedToCreateArchive(let output):
return output.isEmpty ? "Failed to create the .mcworld archive." : output
}
}
}
nonisolated static func exportWorld(_ item: MinecraftContentItem, to destinationURL: URL) throws {
guard item.contentType == .world else {
throw ExportError.unsupportedContentType
}
let fileManager = FileManager.default
let normalizedDestinationURL = destinationURL.standardizedFileURL
let archiveURL = normalizedDestinationURL.pathExtension.lowercased() == "mcworld"
? normalizedDestinationURL
: normalizedDestinationURL.appendingPathExtension("mcworld")
let temporaryArchiveURL = fileManager.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mcworld")
defer {
try? fileManager.removeItem(at: temporaryArchiveURL)
}
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
process.currentDirectoryURL = item.folderURL
process.arguments = [
"-c",
"-k",
"--norsrc",
".",
temporaryArchiveURL.path
]
let outputPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = outputPipe
try process.run()
process.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard process.terminationStatus == 0 else {
throw ExportError.failedToCreateArchive(output)
}
if fileManager.fileExists(atPath: archiveURL.path) {
try fileManager.removeItem(at: archiveURL)
}
try fileManager.moveItem(at: temporaryArchiveURL, to: archiveURL)
}
nonisolated static func suggestedFilename(for item: MinecraftContentItem) -> String {
let baseName = sanitizedFilename(item.displayName.isEmpty ? item.folderName : item.displayName)
return "\(baseName).mcworld"
}
nonisolated private static func sanitizedFilename(_ value: String) -> String {
let invalidCharacters = CharacterSet(charactersIn: "/:\\?%*|\"<>")
let components = value.components(separatedBy: invalidCharacters)
let collapsed = components.joined(separator: " ")
.replacingOccurrences(of: "\n", with: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedWhitespace = collapsed.replacingOccurrences(
of: "\\s+",
with: " ",
options: .regularExpression
)
return normalizedWhitespace.isEmpty ? "Minecraft World" : normalizedWhitespace
}
}