├── .gitignore
├── MountDiskimage.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── MountDiskimage.xccheckout
├── xcshareddata
│ └── xcschemes
│ │ └── mount_diskimage.xcscheme
└── project.pbxproj
├── LICENSE.txt
├── README.md
├── AttachInfo.swift
└── main.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata
2 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2 | Version 2, December 2004
3 |
4 | Copyright (C) 2004 Sam Hocevar
5 |
6 | Everyone is permitted to copy and distribute verbatim or modified
7 | copies of this license document, and changing it is allowed as long
8 | as the name is changed.
9 |
10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12 |
13 | 0. You just DO WHAT THE FUCK YOU WANT TO.
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Auto-Mount Disk Images
2 | ======================
3 |
4 | MacOS disk images are helpful if you want to create a sub-file system with different case
5 | sensitivity options, or if you want to store large files in a Time-Machine-friendly way. I
6 | use a disk image to store my VMware files, because they are huge and receive relatively
7 | small changes when the VM runs. When wrapped in a sparse bundle image, Time Machine gets
8 | those changes served in much smaller portions, which conserves backup space.
9 |
10 | However, to provide a seamless experience, those kinds of disk images should not need user
11 | management, so auto-mounting is needed. MacOS already supports auto-mounting, but only for
12 | device-backed file systems. This project adds support for disk image auto-mounting.
13 |
14 | To use it, add `mount_diskimage` as an executable mount map to of `/etc/auto_master` similar
15 | to this (path names will vary for you):
16 |
17 | /System/Volumes/Data/images /var/root/mount_diskimage -hidefromfinder
18 |
19 | Then run `automount -c` for changes to become effective. You can configure the disk images
20 | to be automounted in the `imageMounts` variable right in the code. Sorry, no config file.
21 |
22 | ___
23 | This work is licensed under the [WTFPL](http://www.wtfpl.net/), so you can do anything you
24 | want with it.
25 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/project.xcworkspace/xcshareddata/MountDiskimage.xccheckout:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDESourceControlProjectFavoriteDictionaryKey
6 |
7 | IDESourceControlProjectIdentifier
8 | B42C71ED-B655-42D9-9224-86C1C3E3783C
9 | IDESourceControlProjectName
10 | MountDiskimage
11 | IDESourceControlProjectOriginsDictionary
12 |
13 | 9675E8F691605C99E08E80C9D2DC5FDBA4569444
14 | github.com:mroi/mount-diskimage.git
15 |
16 | IDESourceControlProjectPath
17 | MountDiskimage.xcodeproj
18 | IDESourceControlProjectRelativeInstallPathDictionary
19 |
20 | 9675E8F691605C99E08E80C9D2DC5FDBA4569444
21 | ../..
22 |
23 | IDESourceControlProjectURL
24 | github.com:mroi/mount-diskimage.git
25 | IDESourceControlProjectVersion
26 | 111
27 | IDESourceControlProjectWCCIdentifier
28 | 9675E8F691605C99E08E80C9D2DC5FDBA4569444
29 | IDESourceControlProjectWCConfigurations
30 |
31 |
32 | IDESourceControlRepositoryExtensionIdentifierKey
33 | public.vcs.git
34 | IDESourceControlWCCIdentifierKey
35 | 9675E8F691605C99E08E80C9D2DC5FDBA4569444
36 | IDESourceControlWCCName
37 | mount-diskimage
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/AttachInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | Decode the property list resulting from `hdiutil attach` and extract the primary mountable volume.
5 | */
6 | struct AttachInfo: Decodable {
7 | let device: String
8 | let type: String
9 | let mounted: Bool
10 |
11 | private enum AttachKeys: String, CodingKey {
12 | case systemEntities = "system-entities"
13 | }
14 | private enum EntityKeys: String, CodingKey {
15 | case device = "dev-entry"
16 | case mountable = "potentially-mountable"
17 | case volumeType = "volume-kind"
18 | case mountPoint = "mount-point"
19 | }
20 |
21 | init(from data: Data) throws {
22 | let decoder = PropertyListDecoder()
23 | self = try decoder.decode(AttachInfo.self, from: data)
24 | }
25 |
26 | init(from decoder: Decoder) throws {
27 | let allowedVolumeTypes = ["apfs", "hfs"]
28 | var entities = try decoder
29 | .container(keyedBy: AttachKeys.self)
30 | .nestedUnkeyedContainer(forKey: .systemEntities)
31 | while !entities.isAtEnd {
32 | let entity = try entities.nestedContainer(keyedBy: EntityKeys.self)
33 | let mountable = try entity.decodeIfPresent(Bool.self, forKey: .mountable) ?? false
34 | let volumeType = try entity.decodeIfPresent(String.self, forKey: .volumeType) ?? ""
35 | if mountable && allowedVolumeTypes.contains(volumeType) {
36 | device = try entity.decode(String.self, forKey: .device)
37 | type = volumeType
38 | mounted = (try entity.decodeIfPresent(String.self, forKey: .mountPoint)) != nil
39 | return
40 | }
41 | }
42 | let context = DecodingError.Context(codingPath: [AttachKeys.systemEntities], debugDescription: "no matching mountable volume found")
43 | throw DecodingError.keyNotFound(EntityKeys.device, context)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/xcshareddata/xcschemes/mount_diskimage.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
54 |
56 |
62 |
63 |
64 |
65 |
73 |
74 |
76 |
77 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/main.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import os
3 |
4 | let imageMounts = [
5 | "Virtual Machines": "/Users/Michael/Library/VM/VM.sparsebundle"
6 | ].filter {
7 | // only include existing images
8 | FileManager.default.fileExists(atPath: $0.value)
9 | }
10 |
11 | switch CommandLine.argc {
12 | case 1:
13 | print(imageMounts.keys.joined(separator: "\n"))
14 |
15 | case 2:
16 | guard let image = imageMounts[CommandLine.arguments[1]] else {
17 | exit(EX_USAGE)
18 | }
19 |
20 | // drop privileges to the owner of the disk image
21 | let attributes = try? FileManager.default.attributesOfItem(atPath: image)
22 | if let group = (attributes?[.groupOwnerAccountID] as? NSNumber)?.uint32Value {
23 | setgid(group)
24 | }
25 | if let user = (attributes?[.ownerAccountID] as? NSNumber)?.uint32Value {
26 | setuid(user)
27 | }
28 |
29 | // compact the disk image for about 10% of mount attempts
30 | let hdiutil = URL(fileURLWithPath: "/usr/bin/hdiutil")
31 | if Float.random(in: 0...1) < 0.1 {
32 | guard let process = try? Process.run(hdiutil, arguments: ["compact", image, "-quiet"]) else {
33 | exit(EX_OSERR)
34 | }
35 | process.waitUntilExit()
36 |
37 | if process.terminationStatus == EX_OK {
38 | os_log("compacted disk image ‘%{public}s’", image)
39 | }
40 | // compaction expectedly fails when the image is already attached
41 | }
42 |
43 | // attach the image without mounting it
44 | let result: AttachInfo
45 | do {
46 | let process = Process()
47 | let pipe = Pipe()
48 | process.executableURL = hdiutil
49 | process.arguments = ["attach", image, "-plist", "-nomount", "-noverify", "-noautofsck"]
50 | process.standardOutput = pipe
51 | try process.run()
52 | process.waitUntilExit()
53 |
54 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
55 | let status = process.terminationStatus
56 | if !(data.count > 0 && status == EX_OK) {
57 | os_log("attaching the disk image ‘%{public}s’ failed with error code %d", image, status)
58 | exit(EX_UNAVAILABLE)
59 | }
60 |
61 | result = try AttachInfo(from: data)
62 | }
63 | catch {
64 | exit(EX_OSERR)
65 | }
66 |
67 | // now that we know the details, definitely print output when finished
68 | defer {
69 | print("-fstype=\(result.type),nobrowse,nodev,nosuid :\(result.device)")
70 | }
71 |
72 | // early exit if the image has been mounted by previous automount invocations
73 | if result.mounted { break }
74 |
75 | // run filesystem check if the image is not mounted
76 | do {
77 | let process = Process()
78 | process.executableURL = URL(fileURLWithPath: "/sbin/fsck_\(result.type)")
79 | process.arguments = ["-q", result.device]
80 | process.standardOutput = FileHandle.nullDevice
81 | try process.run()
82 | process.waitUntilExit()
83 |
84 | let status = process.terminationStatus
85 | if status != EX_OK {
86 | os_log("the file system in disk image ‘%{public}s’ needs repair, error code: %d", image, status)
87 |
88 | let process = Process()
89 | process.executableURL = URL(fileURLWithPath: "/usr/sbin/diskutil")
90 | process.arguments = ["repairVolume", result.device]
91 | process.standardOutput = FileHandle.nullDevice
92 | try process.run()
93 | process.waitUntilExit()
94 |
95 | let status = process.terminationStatus
96 | if status != EX_OK {
97 | os_log("the file system could not be repaired, error code: %d", status)
98 | }
99 | }
100 | }
101 | catch {
102 | exit(EX_OSERR)
103 | }
104 |
105 | default:
106 | exit(EX_USAGE)
107 | }
108 |
--------------------------------------------------------------------------------
/MountDiskimage.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 4C7F947723B67FE6008BCB0C /* AttachInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7F947623B67FE6008BCB0C /* AttachInfo.swift */; };
11 | 4CC8B6281CA821D70015576D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC8B6271CA821D70015576D /* main.swift */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXFileReference section */
15 | 4C7F947623B67FE6008BCB0C /* AttachInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachInfo.swift; sourceTree = ""; };
16 | 4CC8B6241CA821D70015576D /* mount_diskimage */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = mount_diskimage; sourceTree = BUILT_PRODUCTS_DIR; };
17 | 4CC8B6271CA821D70015576D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
18 | 4CC8B62D1CA828E00015576D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
19 | /* End PBXFileReference section */
20 |
21 | /* Begin PBXGroup section */
22 | 089C166AFE841209C02AAC07 /* MountDiskimage */ = {
23 | isa = PBXGroup;
24 | children = (
25 | 4CC8B62D1CA828E00015576D /* README.md */,
26 | 4CC8B6271CA821D70015576D /* main.swift */,
27 | 4C7F947623B67FE6008BCB0C /* AttachInfo.swift */,
28 | 4CC8B6241CA821D70015576D /* mount_diskimage */,
29 | );
30 | name = MountDiskimage;
31 | sourceTree = "";
32 | };
33 | /* End PBXGroup section */
34 |
35 | /* Begin PBXNativeTarget section */
36 | 4CC8B6231CA821D70015576D /* mount_diskimage */ = {
37 | isa = PBXNativeTarget;
38 | buildConfigurationList = 4CC8B6291CA821D70015576D /* Build configuration list for PBXNativeTarget "mount_diskimage" */;
39 | buildPhases = (
40 | 4CC8B6201CA821D70015576D /* Sources */,
41 | );
42 | buildRules = (
43 | );
44 | dependencies = (
45 | );
46 | name = mount_diskimage;
47 | productName = mount_diskimage;
48 | productReference = 4CC8B6241CA821D70015576D /* mount_diskimage */;
49 | productType = "com.apple.product-type.tool";
50 | };
51 | /* End PBXNativeTarget section */
52 |
53 | /* Begin PBXProject section */
54 | 089C1669FE841209C02AAC07 /* Project object */ = {
55 | isa = PBXProject;
56 | attributes = {
57 | BuildIndependentTargetsInParallel = YES;
58 | LastSwiftUpdateCheck = 0730;
59 | LastUpgradeCheck = 1130;
60 | TargetAttributes = {
61 | 4CC8B6231CA821D70015576D = {
62 | CreatedOnToolsVersion = 7.3;
63 | LastSwiftMigration = 0830;
64 | };
65 | };
66 | };
67 | buildConfigurationList = 1DEB913E08733D840010E9CD /* Build configuration list for PBXProject "MountDiskimage" */;
68 | compatibilityVersion = "Xcode 11.0";
69 | developmentRegion = en;
70 | hasScannedForEncodings = 1;
71 | knownRegions = (
72 | en,
73 | Base,
74 | );
75 | mainGroup = 089C166AFE841209C02AAC07 /* MountDiskimage */;
76 | productRefGroup = 089C166AFE841209C02AAC07 /* MountDiskimage */;
77 | projectDirPath = "";
78 | projectRoot = "";
79 | targets = (
80 | 4CC8B6231CA821D70015576D /* mount_diskimage */,
81 | );
82 | };
83 | /* End PBXProject section */
84 |
85 | /* Begin PBXSourcesBuildPhase section */
86 | 4CC8B6201CA821D70015576D /* Sources */ = {
87 | isa = PBXSourcesBuildPhase;
88 | buildActionMask = 2147483647;
89 | files = (
90 | 4CC8B6281CA821D70015576D /* main.swift in Sources */,
91 | 4C7F947723B67FE6008BCB0C /* AttachInfo.swift in Sources */,
92 | );
93 | runOnlyForDeploymentPostprocessing = 0;
94 | };
95 | /* End PBXSourcesBuildPhase section */
96 |
97 | /* Begin XCBuildConfiguration section */
98 | 1DEB913F08733D840010E9CD /* Debug */ = {
99 | isa = XCBuildConfiguration;
100 | buildSettings = {
101 | ALWAYS_SEARCH_USER_PATHS = NO;
102 | DSTROOT = /;
103 | ENABLE_HARDENED_RUNTIME = YES;
104 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
105 | SWIFT_VERSION = 5.0;
106 | };
107 | name = Debug;
108 | };
109 | 1DEB914008733D840010E9CD /* Release */ = {
110 | isa = XCBuildConfiguration;
111 | buildSettings = {
112 | ALWAYS_SEARCH_USER_PATHS = NO;
113 | CODE_SIGN_IDENTITY = "Michael Roitzsch";
114 | DEPLOYMENT_LOCATION = YES;
115 | DEPLOYMENT_POSTPROCESSING = YES;
116 | DSTROOT = /;
117 | ENABLE_HARDENED_RUNTIME = YES;
118 | SWIFT_COMPILATION_MODE = wholemodule;
119 | SWIFT_VERSION = 5.0;
120 | };
121 | name = Release;
122 | };
123 | 4CC8B62A1CA821D70015576D /* Debug */ = {
124 | isa = XCBuildConfiguration;
125 | buildSettings = {
126 | INSTALL_MODE_FLAG = "u+rwX,go-rwx";
127 | INSTALL_PATH = "$HOME/.unison/root-darwin";
128 | OTHER_SWIFT_FLAGS = "-DDEBUG";
129 | PRODUCT_BUNDLE_IDENTIFIER = "de.reactorcontrol.mount-diskimage";
130 | PRODUCT_NAME = "$(TARGET_NAME)";
131 | };
132 | name = Debug;
133 | };
134 | 4CC8B62B1CA821D70015576D /* Release */ = {
135 | isa = XCBuildConfiguration;
136 | buildSettings = {
137 | INSTALL_MODE_FLAG = "u+rwX,go-rwx";
138 | INSTALL_PATH = "$HOME/.unison/root-darwin";
139 | PRODUCT_BUNDLE_IDENTIFIER = "de.reactorcontrol.mount-diskimage";
140 | PRODUCT_NAME = "$(TARGET_NAME)";
141 | };
142 | name = Release;
143 | };
144 | /* End XCBuildConfiguration section */
145 |
146 | /* Begin XCConfigurationList section */
147 | 1DEB913E08733D840010E9CD /* Build configuration list for PBXProject "MountDiskimage" */ = {
148 | isa = XCConfigurationList;
149 | buildConfigurations = (
150 | 1DEB913F08733D840010E9CD /* Debug */,
151 | 1DEB914008733D840010E9CD /* Release */,
152 | );
153 | defaultConfigurationIsVisible = 0;
154 | defaultConfigurationName = Release;
155 | };
156 | 4CC8B6291CA821D70015576D /* Build configuration list for PBXNativeTarget "mount_diskimage" */ = {
157 | isa = XCConfigurationList;
158 | buildConfigurations = (
159 | 4CC8B62A1CA821D70015576D /* Debug */,
160 | 4CC8B62B1CA821D70015576D /* Release */,
161 | );
162 | defaultConfigurationIsVisible = 0;
163 | defaultConfigurationName = Release;
164 | };
165 | /* End XCConfigurationList section */
166 | };
167 | rootObject = 089C1669FE841209C02AAC07 /* Project object */;
168 | }
169 |
--------------------------------------------------------------------------------