98 lines
3.3 KiB
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
|
|
}
|
|
}
|