mirror of
https://github.com/munki/munki.git
synced 2026-05-20 05:08:33 -05:00
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:
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ?? ""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user