Skip to content

Commit ddb8a46

Browse files
authored
feat: adds apfs and noexec (#46)
* feat: adds apfs and noexec * feat: adds better task handling and ejection * fix: only use helper for tmpfs * feat: mountpoint and add button on autocreate fixes #35 * feat: adds disk size validation fixes #36 * feat: tmpdisk cli
1 parent c5db4f3 commit ddb8a46

25 files changed

+1592
-1044
lines changed

Common/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ enum TmpDiskError: Error {
1818
case failed
1919
case helperInvalidated
2020
case helperFailed
21+
case inUse
2122
}

Common/DiskSizeManager.swift

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// DiskSizeManager.swift
3+
// TmpDisk
4+
//
5+
// Created on 06/04/25.
6+
//
7+
// This file is part of TmpDisk.
8+
//
9+
// TmpDisk is free software: you can redistribute it and/or modify
10+
// it under the terms of the GNU General Public License as published by
11+
// the Free Software Foundation, either version 3 of the License, or
12+
// (at your option) any later version.
13+
//
14+
// TmpDisk is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
// GNU General Public License for more details.
18+
//
19+
// You should have received a copy of the GNU General Public License
20+
// along with TmpDisk. If not, see <http://www.gnu.org/licenses/>.
21+
22+
import Foundation
23+
import AppKit
24+
25+
enum DiskSizeUnit: Int {
26+
case mb = 0 // Megabytes
27+
case gb = 1 // Gigabytes
28+
29+
var displayName: String {
30+
switch self {
31+
case .mb:
32+
return "MB"
33+
case .gb:
34+
return "GB"
35+
}
36+
}
37+
}
38+
39+
class DiskSizeManager {
40+
static let shared = DiskSizeManager()
41+
42+
// MARK: - RAM Size Properties
43+
44+
/// Total physical RAM in megabytes
45+
var totalRamSizeMB: Double {
46+
return Double(ProcessInfo.init().physicalMemory) / 1024 / 1024
47+
}
48+
49+
/// Maximum allowed size for TmpFS volumes (50% of RAM) in megabytes
50+
var maxTmpFSSizeMB: Double {
51+
return totalRamSizeMB * 0.5
52+
}
53+
54+
// MARK: - RAM Size Percentages
55+
56+
/// Get RAM size for a specific percentage in MB
57+
func ramSizeForPercentage(_ percentage: Double) -> Double {
58+
return totalRamSizeMB * percentage
59+
}
60+
61+
/// Get the common RAM size percentages (10%, 25%, 50%, 75%)
62+
func commonRamSizePercentages() -> [Double] {
63+
return [0.1, 0.25, 0.5, 0.75]
64+
}
65+
66+
// MARK: - Unit Conversion
67+
68+
/// Convert size from MB to GB
69+
func convertMBtoGB(_ sizeMB: Double) -> Double {
70+
return sizeMB / 1000.0
71+
}
72+
73+
/// Convert size from GB to MB
74+
func convertGBtoMB(_ sizeGB: Double) -> Double {
75+
return sizeGB * 1000.0
76+
}
77+
78+
/// Convert size between units
79+
func convertSize(_ size: Double, from fromUnit: DiskSizeUnit, to toUnit: DiskSizeUnit) -> Double {
80+
switch (fromUnit, toUnit) {
81+
case (.mb, .gb):
82+
return convertMBtoGB(size)
83+
case (.gb, .mb):
84+
return convertGBtoMB(size)
85+
case (.mb, .mb):
86+
return size
87+
case (.gb, .gb):
88+
return size
89+
}
90+
}
91+
92+
// MARK: - String Formatting
93+
94+
/// Format size in MB as a string in the given unit
95+
func formatSize(_ sizeMB: Double, in unit: DiskSizeUnit) -> String {
96+
switch unit {
97+
case .mb:
98+
return String(Int(sizeMB))
99+
case .gb:
100+
return String(format: "%.2f", convertMBtoGB(sizeMB))
101+
}
102+
}
103+
104+
// MARK: - Validation
105+
106+
/// Validates if the size is within the TmpFS limit
107+
/// - Parameters:
108+
/// - size: The size to validate
109+
/// - unit: The unit of the provided size
110+
/// - Returns: A tuple containing (isValid, correctedSize)
111+
func validateDiskSize(_ size: Double, in unit: DiskSizeUnit, isTmpFS: Bool = false) -> (isValid: Bool, correctedSizeMB: Double) {
112+
let sizeInMB = unit == .mb ? size : convertGBtoMB(size)
113+
114+
if isTmpFS && sizeInMB > maxTmpFSSizeMB {
115+
return (false, maxTmpFSSizeMB)
116+
} else if sizeInMB < 0 {
117+
return (false, 1)
118+
} else if sizeInMB > ( totalRamSizeMB - 2048) {
119+
// Hold back at least 2GB
120+
return (false, totalRamSizeMB - 2048)
121+
}
122+
123+
return (true, sizeInMB)
124+
}
125+
126+
/// Shows a warning alert about the TmpFS size limitation
127+
func showTmpFSSizeWarning() {
128+
let alert = NSAlert()
129+
alert.messageText = NSLocalizedString("TmpFS volumes are limited to 50% of RAM. Setting to maximum allowed value.", comment: "")
130+
alert.alertStyle = .informational
131+
alert.addButton(withTitle: "OK")
132+
alert.runModal()
133+
}
134+
135+
func showInsufficientRamWarning() {
136+
let alert = NSAlert()
137+
alert.messageText = NSLocalizedString("Insufficient RAM to allocate to TmpDisk. Please reduce the size.", comment: "")
138+
alert.alertStyle = .warning
139+
alert.addButton(withTitle: "OK")
140+
alert.runModal()
141+
}
142+
}

Common/FileSystemManager.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// FileSystemManager.swift
3+
// TmpDisk
4+
//
5+
// Created by Tim on 4/3/25.
6+
//
7+
// This file is part of TmpDisk.
8+
//
9+
// TmpDisk is free software: you can redistribute it and/or modify
10+
// it under the terms of the GNU General Public License as published by
11+
// the Free Software Foundation, either version 3 of the License, or
12+
// (at your option) any later version.
13+
//
14+
// TmpDisk is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
// GNU General Public License for more details.
18+
//
19+
// You should have received a copy of the GNU General Public License
20+
// along with TmpDisk. If not, see <http://www.gnu.org/licenses/>.
21+
22+
import Foundation
23+
24+
struct FileSystemType: Identifiable, Hashable {
25+
let id = UUID()
26+
let name: String
27+
let description: String
28+
29+
func hash(into hasher: inout Hasher) {
30+
hasher.combine(name)
31+
}
32+
33+
static func == (lhs: FileSystemType, rhs: FileSystemType) -> Bool {
34+
return lhs.name == rhs.name
35+
}
36+
}
37+
38+
class FileSystemManager {
39+
// All filesystem types with descriptions
40+
private static let allFileSystems: [FileSystemType] = [
41+
FileSystemType(name: "APFS", description: "Apple File System - Suggested"),
42+
FileSystemType(name: "APFSX", description: "Apple File System (Case Sensitive)"),
43+
FileSystemType(name: "HFS+", description: "Mac OS Extended - Legacy"),
44+
FileSystemType(name: "TMPFS", description: "TmpFS - Requires Admin or TmpDisk Helper"),
45+
FileSystemType(name: "HFSX", description: "Mac OS Extended (Case-sensitive)"),
46+
FileSystemType(name: "JHFS+", description: "Mac OS Extended (Journaled)"),
47+
FileSystemType(name: "JHFSX", description: "Mac OS Extended (Case-sensitive, Journaled)"),
48+
]
49+
50+
// Get filesystem types appropriate for the current OS version
51+
static func availableFileSystems() -> [FileSystemType] {
52+
var available = allFileSystems
53+
54+
if #available(macOS 10.13, *) {
55+
// Keep APFS for macOS 10.13+ (High Sierra and later)
56+
} else {
57+
// Remove APFS from available options for earlier macOS versions
58+
available.removeAll { $0.name == "APFS" || $0.name == "APFSX" }
59+
}
60+
61+
return available
62+
}
63+
64+
// Get the default FS
65+
static func defaultFileSystemName() -> String {
66+
return self.availableFileSystems().first!.name
67+
}
68+
69+
// Get the descriptions of avaialable file systems
70+
static func availableFileSystemDescriptions() -> [String] {
71+
return availableFileSystems().map(\.self.description)
72+
}
73+
74+
static func isTmpFS(_ fileSystemName: String) -> Bool {
75+
return fileSystemName == "TMPFS"
76+
}
77+
78+
static func isAPFS(_ fileSystemName: String) -> Bool {
79+
return fileSystemName == "APFS" || fileSystemName == "APFSX"
80+
}
81+
82+
static func description(for fileSystemName: String) -> String? {
83+
return availableFileSystems().first(where: { $0.name == fileSystemName })?.description
84+
}
85+
}

Common/Protocols.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ import Foundation
99

1010
@objc protocol TmpDiskCreator {
1111
func createTmpDisk(_ command: String, onCreate: @escaping (Bool) -> Void)
12+
func ejectTmpDisk(_ command: String, onEject: @escaping (Int32) -> Void)
1213
func uninstall()
1314
}

Common/TmpDiskVolume.swift

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,89 @@
66
//
77

88
import Foundation
9+
import AppKit
910

1011
struct TmpDiskVolume: Hashable, Codable {
1112
var name: String = ""
1213
var size: Int = 16
1314
var autoCreate: Bool = false
15+
var fileSystem: String
1416
var indexed: Bool = false
17+
var noExec: Bool = false
1518
var hidden: Bool = false
16-
var tmpFs: Bool = false
17-
var caseSensitive: Bool = false
18-
var journaled: Bool = false
1919
var warnOnEject: Bool = false
2020
var folders: [String] = []
2121
var icon: String?
22+
var mountPoint: String?
23+
24+
init() {
25+
self.fileSystem = FileSystemManager.defaultFileSystemName()
26+
}
27+
28+
init(name: String, size: Int, fileSystem: String? = nil) {
29+
self.name = name
30+
self.size = size
31+
self.fileSystem = fileSystem ?? FileSystemManager.defaultFileSystemName()
32+
}
33+
34+
35+
init?(from dictionary: Dictionary<String, Any>) {
36+
guard let name = dictionary["name"] as? String,
37+
let size = dictionary["size"] as? Int,
38+
let indexed = dictionary["indexed"] as? Bool,
39+
let hidden = dictionary["hidden"] as? Bool
40+
else { return nil }
41+
42+
let warnOnEject = dictionary["warnOnEject"] as? Bool ?? false
43+
let folders = dictionary["folders"] as? [String] ?? []
44+
let icon = dictionary["icon"] as? String
45+
let noExec = dictionary["noExec"] as? Bool ?? false
46+
let mountPoint = dictionary["mountPoint"] as? String
47+
48+
let fileSystem: String
49+
50+
if let fs = dictionary["fileSystem"] as? String {
51+
fileSystem = fs
52+
} else {
53+
let tmpFs = dictionary["tmpFs"] as? Bool
54+
let caseSensitive = dictionary["caseSensitive"] as? Bool
55+
let journaled = dictionary["journaled"] as? Bool
56+
57+
// We're going to use HSF+ for legacy tmpdisks
58+
if tmpFs ?? false {
59+
fileSystem = "TMPFS"
60+
} else if caseSensitive ?? false && journaled ?? false {
61+
fileSystem = "JHFSX"
62+
} else if caseSensitive ?? false {
63+
fileSystem = "HFSX"
64+
} else if journaled ?? false {
65+
fileSystem = "JHFS+"
66+
} else {
67+
fileSystem = "HFS+"
68+
}
69+
}
70+
71+
self.name = name
72+
self.size = size
73+
self.autoCreate = true
74+
self.fileSystem = fileSystem
75+
self.indexed = indexed
76+
self.hidden = hidden
77+
self.noExec = noExec
78+
self.warnOnEject = warnOnEject
79+
self.folders = folders
80+
if let icon = icon, icon != "" {
81+
self.icon = icon
82+
}
83+
if let mountPoint = mountPoint, mountPoint != "" {
84+
self.mountPoint = mountPoint
85+
}
86+
}
2287

2388
func path() -> String {
24-
if tmpFs {
89+
if let mountPoint = self.mountPoint {
90+
return mountPoint
91+
} else if FileSystemManager.isTmpFS(fileSystem) {
2592
return "\(TmpDiskManager.rootFolder)/\(name)"
2693
}
2794
return "/Volumes/\(name)"
@@ -37,12 +104,12 @@ struct TmpDiskVolume: Hashable, Codable {
37104
"size": size,
38105
"indexed": indexed,
39106
"hidden": hidden,
40-
"tmpFs": tmpFs,
41-
"caseSensitive": caseSensitive,
42-
"journaled": journaled,
107+
"filesystem": fileSystem,
108+
"noExec": noExec,
43109
"warnOnEject": warnOnEject,
44110
"folders": folders,
45111
"icon": icon ?? "",
112+
"mountPoint": mountPoint ?? ""
46113
]
47114
}
48115

@@ -56,4 +123,14 @@ struct TmpDiskVolume: Hashable, Codable {
56123
}
57124
return false
58125
}
126+
127+
func isMounted() -> Bool {
128+
let mountPoint = self.path()
129+
if let mountedVolumes = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil) {
130+
let mountedPaths = mountedVolumes.map { $0.path }
131+
return mountedPaths.contains { $0 == mountPoint }
132+
}
133+
134+
return false
135+
}
59136
}

0 commit comments

Comments
 (0)