More implementation of updatecheck/download functions; refactoring of parts of network/fetch; adding support for localizedDescription to MunkiError and FetchError

This commit is contained in:
Greg Neagle
2024-08-16 09:14:46 -07:00
parent 17989ccbc5
commit f0af5a885f
7 changed files with 526 additions and 17 deletions
@@ -24,6 +24,10 @@
C01364592C3265D6008DB215 /* display.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364572C3265D6008DB215 /* display.swift */; };
C01792D32C6BC81B008CBC22 /* fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D22C6BC81B008CBC22 /* fetch.swift */; };
C01792D42C6BC81B008CBC22 /* fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D22C6BC81B008CBC22 /* fetch.swift */; };
C01792D62C6E58D1008CBC22 /* download.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D52C6E58D1008CBC22 /* download.swift */; };
C01792D72C6E58D1008CBC22 /* download.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D52C6E58D1008CBC22 /* download.swift */; };
C01792D92C6EC09C008CBC22 /* urls.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D82C6EC09C008CBC22 /* urls.swift */; };
C01792DA2C6EC09C008CBC22 /* urls.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01792D82C6EC09C008CBC22 /* urls.swift */; };
C0209B862C63C9FD007664A0 /* stoprequested.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0209B852C63C9FD007664A0 /* stoprequested.swift */; };
C0209B872C63C9FD007664A0 /* stoprequested.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0209B852C63C9FD007664A0 /* stoprequested.swift */; };
C0209B892C63DDF1007664A0 /* info.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0209B882C63DDF1007664A0 /* info.swift */; };
@@ -292,6 +296,8 @@
C01364532C321FE7008DB215 /* dmgutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dmgutils.swift; sourceTree = "<group>"; };
C01364572C3265D6008DB215 /* display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = display.swift; sourceTree = "<group>"; };
C01792D22C6BC81B008CBC22 /* fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fetch.swift; sourceTree = "<group>"; };
C01792D52C6E58D1008CBC22 /* download.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = download.swift; sourceTree = "<group>"; };
C01792D82C6EC09C008CBC22 /* urls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = urls.swift; sourceTree = "<group>"; };
C0209B852C63C9FD007664A0 /* stoprequested.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = stoprequested.swift; sourceTree = "<group>"; };
C0209B882C63DDF1007664A0 /* info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = info.swift; sourceTree = "<group>"; };
C0209B8F2C63E3B7007664A0 /* launchapp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = launchapp; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -618,6 +624,7 @@
isa = PBXGroup;
children = (
C07AED6A2C66F56C00DE6119 /* manifests.swift */,
C01792D52C6E58D1008CBC22 /* download.swift */,
);
path = updatecheck;
sourceTree = "<group>";
@@ -628,6 +635,7 @@
C07AED6E2C67DF6B00DE6119 /* gurl.swift */,
C07AED712C67F77000DE6119 /* sslerrors.swift */,
C01792D22C6BC81B008CBC22 /* fetch.swift */,
C01792D82C6EC09C008CBC22 /* urls.swift */,
);
path = network;
sourceTree = "<group>";
@@ -1001,6 +1009,7 @@
C07AED662C66D0A000DE6119 /* facts.swift in Sources */,
C030A98F2C39C135007F0B34 /* munkihash.swift in Sources */,
C07AED722C67F77000DE6119 /* sslerrors.swift in Sources */,
C01792D62C6E58D1008CBC22 /* download.swift in Sources */,
C0D00FB62C45BCB90021DA9C /* errors.swift in Sources */,
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */,
C0D9C2972C6012C80019A067 /* dmg.swift in Sources */,
@@ -1025,6 +1034,7 @@
C07AED6F2C67DF6B00DE6119 /* gurl.swift in Sources */,
C07AED632C66CFBD00DE6119 /* appinventory.swift in Sources */,
C043ED232C483EEE0047C025 /* rmpkgs.swift in Sources */,
C01792D92C6EC09C008CBC22 /* urls.swift in Sources */,
C0209B862C63C9FD007664A0 /* stoprequested.swift in Sources */,
C07074DF2C33B9A000B86310 /* reports.swift in Sources */,
);
@@ -1049,6 +1059,7 @@
C0D00FB12C458EAA0021DA9C /* version.swift in Sources */,
C030A9C32C41B581007F0B34 /* pkginfoOptions.swift in Sources */,
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */,
C01792D72C6E58D1008CBC22 /* download.swift in Sources */,
C0209B872C63C9FD007664A0 /* stoprequested.swift in Sources */,
C0D9C29B2C607D390019A067 /* xattr.swift in Sources */,
C030A9BC2C3F4CF4007F0B34 /* munkiimportlib.swift in Sources */,
@@ -1060,6 +1071,7 @@
C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */,
C07A6FBE2C2B5AF000090743 /* prefs.swift in Sources */,
C07A6FBA2C2B5ADE00090743 /* main.swift in Sources */,
C01792DA2C6EC09C008CBC22 /* urls.swift in Sources */,
C030A9B72C3DF6D0007F0B34 /* fileutils.swift in Sources */,
C07074DD2C33AE5F00B86310 /* munkilog.swift in Sources */,
C07A6FBF2C2B5AF400090743 /* constants.swift in Sources */,
+7
View File
@@ -34,6 +34,13 @@ class MunkiError: Error, CustomStringConvertible {
}
}
extension MunkiError: LocalizedError {
// cheap hack
var errorDescription: String? {
return message
}
}
struct UserCancelled: Error {
// an exception to throw when user cancels
}
+2 -2
View File
@@ -660,7 +660,7 @@ func doInstallsAndRemovals(onlyUnattended: Bool = false) async -> Int {
// this is janky because it relies on stuff being recorded to the report
var optionalInstalls = installInfo["optional_installs"] as? [PlistDict] ?? [PlistDict]()
if !optionalInstalls.isEmpty {
if let removalResults = Report.shared.retreive(key: "RemovalResults") as? [PlistDict] {
if let removalResults = Report.shared.retrieve(key: "RemovalResults") as? [PlistDict] {
for (index, optionalInstall) in optionalInstalls.enumerated() {
let optionalInstallName = optionalInstall["name"] as? String ?? "<unknown>"
for removal in removalResults {
@@ -680,7 +680,7 @@ func doInstallsAndRemovals(onlyUnattended: Bool = false) async -> Int {
}
}
}
if let installResults = Report.shared.retreive(key: "InstallResults") as? [PlistDict] {
if let installResults = Report.shared.retrieve(key: "InstallResults") as? [PlistDict] {
for (index, optionalInstall) in optionalInstalls.enumerated() {
let optionalInstallName = optionalInstall["name"] as? String ?? "<unknown>"
let optionalInstallVersion = optionalInstall["version_to_install"] as? String ?? ""
+82 -14
View File
@@ -23,6 +23,23 @@ enum FetchError: Error {
case verification
}
extension FetchError: LocalizedError {
var errorDescription: String? {
switch self {
case let .connection(errorCode, description):
return "Connection error \(errorCode): \(description)"
case let .http(errorCode, description):
return "HTTP error \(errorCode): \(description)"
case let .download(errorCode, description):
return "Download error \(errorCode): \(description)"
case let .fileSystem(description):
return "File system error: \(description)"
case .verification:
return "Checksum verification error"
}
}
}
func storeCachedChecksum(toPath path: String, hash: String? = nil) -> String? {
let fhash: String = if let hash {
hash
@@ -122,6 +139,11 @@ func headerDictFromList(_ strList: [String]?) -> [String: String] {
return headerDict
}
func runMiddleware(options: GurlOptions, pkginfo _: PlistDict?) -> GurlOptions {
// placeholder function
return options
}
func getURL(
_ url: String,
destinationPath: String,
@@ -159,7 +181,7 @@ func getURL(
let ignoreSystemProxy = pref("IgnoreSystemProxies") as? Bool ?? false
let options = GurlOptions(
var options = GurlOptions(
url: url,
destinationPath: tempDownloadPath,
additionalHeaders: headerDictFromList(customHeaders),
@@ -172,7 +194,7 @@ func getURL(
)
// TODO: middleware support
// (which will use pkginfo)
options = runMiddleware(options: options, pkginfo: pkginfo)
let session = Gurl(options: options)
var displayMessage = message
@@ -526,7 +548,7 @@ func getResourceIfChangedAtomically(
return changed
}
func munkiResource(
func fetchMunkiResourceByURL(
_ url: String,
destinationPath: String,
message: String = "",
@@ -535,7 +557,7 @@ func munkiResource(
verify: Bool = false,
pkginfo: PlistDict? = nil
) throws -> Bool {
// The high-level function for getting resources from the Munki repo.
// A high-level function for getting resources from the Munki repo.
// Gets a given URL from the Munki server.
// Adds any additional headers to the request if present
// Throws a FetchError if there's an error
@@ -549,6 +571,7 @@ func munkiResource(
// <string>another-custom-header: bar value</string>
// </array>
let customHeaders = pref(ADDITIONAL_HTTP_HEADERS_KEY) as? [String]
return try getResourceIfChangedAtomically(
url,
destinationPath: destinationPath,
@@ -561,9 +584,45 @@ func munkiResource(
)
}
func getDataFromURL(_ url: String) throws -> Data? {
// Returns data from url as string. We use the existing
// munkiResource function so any custom
enum MunkiResourceType: String {
case catalog = "catalogs"
case clientResource = "client_resources"
case icon = "icons"
case manifest = "manifests"
case package = "pkgs"
}
func fetchMunkiResource(
kind: MunkiResourceType,
name: String,
destinationPath: String,
message: String = "",
resume: Bool = false,
expectedHash: String? = nil,
verify: Bool = false,
pkginfo: PlistDict? = nil
) throws -> Bool {
// An even higher-level function for getting resources from the Munki repo.
guard let url = munkiRepoURL(kind.rawValue, resource: name) else {
throw FetchError.connection(
errorCode: -1,
description: "Could not encode all characters in URL"
)
}
return try fetchMunkiResourceByURL(
url,
destinationPath: destinationPath,
message: message,
resume: resume,
expectedHash: expectedHash,
verify: verify,
pkginfo: pkginfo
)
}
func getDataFromServer(_ url: String) throws -> Data? {
// Returns data from server as string.
// We use the existing fetchMunkiResource function so any custom
// authentication/authorization headers are used
// (including, eventually, middleware-generated headers)
// May throw a FetchError
@@ -573,20 +632,29 @@ func getDataFromURL(_ url: String) throws -> Data? {
return nil
}
defer { try? FileManager.default.removeItem(atPath: tmpDir) }
let urlDataPath = (tmpDir as NSString).appendingPathComponent("urldata")
_ = try munkiResource(url, destinationPath: urlDataPath)
return FileManager.default.contents(atPath: urlDataPath)
let tempDataPath = (tmpDir as NSString).appendingPathComponent("urldata")
_ = try fetchMunkiResourceByURL(
url, destinationPath: tempDataPath
)
return FileManager.default.contents(atPath: tempDataPath)
}
func checkServer(_ urlString: String) -> (Int, String) {
func checkServer(_ urlString: String = "") -> (Int, String) {
// A function we can call to check to see if the server is
// available before we kick off a full run. This can be fooled by
// ISPs that return results for non-existent web servers...
// Returns a tuple (exitCode, exitDescription)
guard let url = URL(string: urlString) else {
return (-1, "Invalid url string")
let serverURL: String = if !urlString.isEmpty {
urlString
} else {
munkiRepoURL() ?? ""
}
guard let url = URL(string: serverURL) else {
return (-1, "Invalid URL")
}
if ["http", "https"].contains(url.scheme) {
// pass
} else if url.scheme == "file" {
@@ -601,7 +669,7 @@ func checkServer(_ urlString: String) -> (Int, String) {
return (-1, "Unsupported URL scheme: \(url.scheme ?? "<none>")")
}
do {
_ = try getDataFromURL(urlString)
_ = try getDataFromServer(url.absoluteString)
} catch let err as FetchError {
switch err {
case let .connection(errorCode, description):
+59
View File
@@ -0,0 +1,59 @@
//
// urls.swift
// munki
//
// Created by Greg Neagle on 8/15/24.
//
import Foundation
func composedURLWithBase(_ baseURLString: String, adding path: String) -> String {
let baseURL = URL(string: baseURLString)
let composedURL = URL(string: path, relativeTo: baseURL)
return composedURL?.absoluteString ?? ""
}
func munkiRepoURL(_ type: String = "", resource: String = "") -> String? {
// we could use composedURLWithBase, but that doesn't handle
// URLs in the format of CGI invocations correctly, and would not
// be consistent with the behavior of the Python version of Munki
// So instead we'll do simple string concatenation
// (with percent-encoding of the resource path)
let munkiBaseURL = pref("SoftwareRepoURL") as? String ?? ""
if type.isEmpty {
return munkiBaseURL
}
let map = [
"catalogs": "CatalogURL",
"client_resources": "ClientResourceURL",
"icons": "IconURL",
"manifests": "ManifestURL",
"pkgs": "PackageURL",
]
guard let encodedType = (type as NSString).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
// encoding failed
return nil
}
// we're not actually handling errors in percent-encoding
var typeURL = munkiBaseURL + "/" + encodedType + "/"
// if a more specific URL has been defined in preferences, use that
if let key = map[type] {
if let testURL = pref(key) as? String {
// add a trailing slash if needed
if testURL.hasSuffix("/") || testURL.hasSuffix("?") {
typeURL = testURL
} else {
typeURL = testURL + "/"
}
}
}
if resource.isEmpty {
return typeURL
}
if let encodedResource = (resource as NSString).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
return typeURL + encodedResource
}
// encoding failed
return nil
}
+1 -1
View File
@@ -34,7 +34,7 @@ class Report {
report[key] = value
}
func retreive(key: String) -> Any? {
func retrieve(key: String) -> Any? {
return report[key]
}
@@ -6,3 +6,366 @@
//
import Foundation
func baseName(_ str: String) -> String {
// Return a basename string.
// Examples:
// "http://foo/bar/path/foo.dmg" => "foo.dmg"
// "/path/foo.dmg" => "foo.dmg"
if let url = URL(string: str) {
return url.lastPathComponent
} else {
return (str as NSString).lastPathComponent
}
}
func getDownloadCachePath(_ urlString: String) -> String {
// For a URL, return the path that the download should cache to.
//
// Returns a string.
return managedInstallsDir(subpath: "Cache/" + baseName(urlString))
}
func enoughDiskSpaceFor(
_ item: PlistDict,
installList: [PlistDict] = [],
uninstalling: Bool = false,
warn: Bool = true,
precaching: Bool = false
) -> Bool {
// Determine if there is enough disk space to download and install
// the installer item.
// fudgefactor is 100MB
let fudgefactor = 102_400 // KBytes
var alreadyDownloadedSize = 0
if let installerItemLocation = item["installer_item_location"] as? String {
let downloadedPath = getDownloadCachePath(installerItemLocation)
if pathExists(downloadedPath) {
alreadyDownloadedSize = getSize(downloadedPath) / 1024 // KBytes
}
}
var installerItemSize = item["installer_item_size"] as? Int ?? 0 // KBytes
var installedSize = item["installed_size"] as? Int ?? installerItemSize // KBytes
if uninstalling {
installedSize = 0
installerItemSize = 0
if let uninstallerItemSize = item["uninstaller_item_size"] as? Int {
installerItemSize = uninstallerItemSize // KBytes
}
}
let diskSpaceNeeded = installerItemSize - alreadyDownloadedSize + installedSize + fudgefactor
var diskSpace = availableDiskSpace() // KBytes
for additionalItem in installList {
// subtract space needed for other items that are to be installed
diskSpace -= (additionalItem["installed_size"] as? Int ?? 0)
}
if diskSpaceNeeded > diskSpace, !precaching {
// try to clear space by deleting some precached items
// TODO: uncache(diskSpaceNeeded - diskSpace)
// now re-calc
diskSpace = availableDiskSpace()
for additionalItem in installList {
// subtract space needed for other items that are to be installed
diskSpace -= (additionalItem["installed_size"] as? Int ?? 0)
}
}
if diskSpace >= diskSpaceNeeded {
return true
}
// we don't have enough space
if warn {
let itemName = item["name"] as? String ?? "<unknown>"
if uninstalling {
displayWarning("There is insufficient disk space to download the uninstaller for \(itemName)")
} else {
displayWarning("There is insufficient disk space to download and install \(itemName)")
}
displayWarning("\(Int(diskSpaceNeeded / 1024))MB needed, \(Int(diskSpace / 1024))MB available")
}
return false
}
func downloadInstallerItem(
_ item: PlistDict,
installInfo: PlistDict,
uninstalling: Bool = false,
precaching: Bool = false
) throws -> Bool {
// Downloads an (un)installer item.
// Returns true if the item was downloaded,
// false if it was already cached.
// Thows an error if there are issues
let downloadItemKey = if uninstalling, item.keys.contains("uninstaller_item_location") {
"uninstaller_item_location"
} else {
"installer_item_location"
}
let itemHashKey = if uninstalling, item.keys.contains("uninstaller_item_location") {
"uninstaller_item_hash"
} else {
"installer_item_hash"
}
guard let location = item[downloadItemKey] as? String else {
throw FetchError.download(
errorCode: -1,
description: "No \(downloadItemKey) in pkginfo"
)
}
// pkginfos support two keys that can essentially override the
// normal URL generation for Munki repo items. But they are not
// commonly used.
let alternatePkgURL: String = if let packageCompleteURL = item["PackageCompleteURL"] as? String {
packageCompleteURL
} else if let packageURL = item["PackageURL"] as? String {
composedURLWithBase(packageURL, adding: location)
} else {
""
}
let pkgName = baseName(location)
displayDebug2("Package name is: \(pkgName)")
if !alternatePkgURL.isEmpty {
displayDebug2("Download URL is: \(alternatePkgURL)")
}
let destinationPath = getDownloadCachePath(location)
displayDebug2("Downloading to: \(destinationPath)")
if !pathExists(destinationPath) {
// check to see if there is enough free space to download and install
let installList = installInfo["managed_installs"] as? [PlistDict] ?? []
if !enoughDiskSpaceFor(
item,
installList: installList,
uninstalling: uninstalling,
precaching: precaching
) {
throw FetchError.download(
errorCode: -1,
description: "Insufficient disk space to download and install \(pkgName)"
)
}
}
displayDetail("Downloading \(pkgName) from \(location)")
let downloadMessage = "Downloading \(pkgName)..."
let expectedHash = item[itemHashKey] as? String
if alternatePkgURL.isEmpty {
return try fetchMunkiResource(
kind: .package,
name: pkgName,
destinationPath: destinationPath,
message: downloadMessage,
resume: true,
expectedHash: expectedHash,
verify: true,
pkginfo: item
)
} else {
// use alternatePkgURL
return try fetchMunkiResourceByURL(
alternatePkgURL,
destinationPath: destinationPath,
message: downloadMessage,
resume: true,
expectedHash: expectedHash,
verify: true,
pkginfo: item
)
}
}
let ICON_HASHES_PLIST_NAME = "_icon_hashes.plist"
func cleanUpIconsDir(keepList: [String] = []) {
// Remove any cached/downloaded icons that aren't in the list of ones
// to keep
let itemsToKeep = keepList + [ICON_HASHES_PLIST_NAME]
let iconsDir = managedInstallsDir(subpath: "icons")
let filemanager = FileManager.default
let dirEnum = filemanager.enumerator(atPath: iconsDir)
while let file = dirEnum?.nextObject() as? String {
let fullPath = (iconsDir as NSString).appendingPathComponent(file)
if !pathIsDirectory(fullPath) {
if !itemsToKeep.contains(file) {
try? filemanager.removeItem(atPath: fullPath)
}
} else if let dirContents = try? filemanager.contentsOfDirectory(atPath: fullPath),
dirContents.isEmpty
{
// remove any empty directories as well
try? filemanager.removeItem(atPath: fullPath)
}
}
}
func getIconHashes() -> [String: String] {
// Attempts to download the dictionary of compiled icon hashes
let iconsHashesPlist = managedInstallsDir(subpath: "icons/\(ICON_HASHES_PLIST_NAME)")
do {
_ = try fetchMunkiResource(
kind: .icon,
name: ICON_HASHES_PLIST_NAME,
destinationPath: iconsHashesPlist,
message: "Getting list of available icons"
)
return try readPlist(fromFile: iconsHashesPlist) as? [String: String] ?? [:]
} catch {
displayDebug1("Error while retreiving icon hashes: \(error.localizedDescription)")
return [String: String]()
}
}
func downloadIcons(_ itemList: [PlistDict]) {
// Attempts to download icons (actually image files) for items in
// itemList
var iconsToKeep = [String]()
let iconsDir = managedInstallsDir(subpath: "icons")
let iconHashes = getIconHashes()
for item in itemList {
var iconName = item["icon_name"] as? String ?? item["name"] as? String ?? "<unknown>"
if (iconName as NSString).pathExtension.isEmpty {
iconName += ".png"
}
iconsToKeep.append(iconName)
let serverHash: String = if let iconHash = item["icon_hash"] as? String {
iconHash
} else {
iconHashes[iconName] ?? "<noserverhash>"
}
let iconPath = (iconsDir as NSString).appendingPathComponent(iconName)
var localHash = "<nolocalhash>"
if pathIsRegularFile(iconPath) {
// have we already downloaded it? If so get the local hash
if let data = try? getXattr(named: XATTR_SHA, atPath: iconPath) {
localHash = String(data: data, encoding: .utf8) ?? "<nolocalhash>"
} else {
// get hash and also store for future use
localHash = storeCachedChecksum(toPath: iconPath) ?? "<nolocalhash>"
}
}
let iconSubDir = (iconPath as NSString).deletingLastPathComponent
if !pathIsDirectory(iconSubDir) {
let success = createMissingDirs(iconSubDir)
if !success {
displayError("Could not create \(iconSubDir)")
continue
}
}
if serverHash != localHash {
// hashes don't match, so download the icon
if !iconHashes.isEmpty, !iconHashes.keys.contains(iconName) {
// if we have a dict of icon hashes, and the icon name is not
// in that dict, then there's no point in attempting to
// download this icon
continue
}
let itemName = item["display_name"] as? String ?? item["name"] as? String ?? "<unknown>"
do {
_ = try fetchMunkiResource(
kind: .icon,
name: iconName,
destinationPath: iconPath,
message: "Getting icon \(iconName) for \(itemName)..."
)
_ = storeCachedChecksum(toPath: iconPath)
} catch {
displayDebug1("Error when retrieving icon \(iconName) from the server: \(error.localizedDescription)")
}
}
}
cleanUpIconsDir(keepList: iconsToKeep)
}
func downloadClientResources() {
// Download client customization resources (if any).
// Munki's preferences can specify an explicit name
// under ClientResourcesFilename
// if that doesn't exist, use the primary manifest name as the
// filename. If that fails, try site_default.zip
// build a list of resource names to request from the server
var filenames = [String]()
if let resourcesName = pref("ClientResourcesFilename") as? String {
if (resourcesName as NSString).pathExtension.isEmpty {
filenames.append(resourcesName + ".zip")
} else {
filenames.append(resourcesName)
}
} else {
// TODO: make a better way to retrieve the current manifest name
if let manifestName = Report.shared.retrieve(key: "ManifestName") as? String {
filenames.append(manifestName + ".zip")
}
}
filenames.append("site_default.zip")
let resourceDir = managedInstallsDir(subpath: "client_resources")
// make sure local resource directory exists
if !pathIsDirectory(resourceDir) {
let success = createMissingDirs(resourceDir)
if !success {
displayError("Could not create \(resourceDir)")
return
}
}
let resourceArchivePath = (resourceDir as NSString).appendingPathComponent("custom.zip")
let message = "Getting client resources..."
var downloadedResourcePath = ""
for filename in filenames {
let resourceURL = munkiRepoURL("client_resources", resource: filename)
do {
_ = try fetchMunkiResource(
kind: .clientResource,
name: filename,
destinationPath: resourceArchivePath,
message: message
)
downloadedResourcePath = resourceArchivePath
break
} catch {
displayDebug1("Could not retrieve client resources with name \(filename): \(error.localizedDescription)")
}
}
if downloadedResourcePath.isEmpty {
// make sure we don't have an old custom.zip hanging around
if pathExists(resourceArchivePath) {
do {
try FileManager.default.removeItem(atPath: resourceArchivePath)
} catch {
displayError("Could not remove stale \(resourceArchivePath): \(error.localizedDescription)")
}
}
}
}
func downloadCatalog(_ catalogName: String) -> String {
// Attempt to download a catalog from the Munki server, Returns the path to
// the downloaded catalog file
let catalogPath = managedInstallsDir(subpath: "catalogs/\(catalogName)")
displayDetail("Getting catalog \(catalogName)...")
let message = "Retrieving catalog \(catalogName)..."
do {
_ = try fetchMunkiResource(
kind: .catalog,
name: catalogName,
destinationPath: catalogPath,
message: message
)
return catalogPath
} catch {
displayError("Could not retrieve catalog \(catalogName) from server: \(error.localizedDescription)")
}
return ""
}
// TODO: precaching support