swiftbashshellswiftui

How can I create a new folder on macOS using a shell command in SwiftUI macOS while ensuring compatibility with the App Sandbox and using bookmarks?


The app is designed to allow the user to select a directory using NSOpenPanel, save access permissions via a security-scoped bookmark, and create a folder in the selected directory using the mkdir bash command executed with Process. The security-scoped bookmark ensures the app can access the directory even after the user closes the app or in subsequent sessions.

However, the code does not work when the App Sandbox is enabled. When the sandbox is removed, the folder creation works without issues. Despite using a security-scoped bookmark to resolve the directory path and access the folder, the app still encounters an "Operation not permitted" error. This indicates that the sandbox restrictions prevent the bash command from operating on the directory, even though permissions should theoretically have been granted via the bookmark.

import SwiftUI

struct ContentView: View {
    
    @State private var folderName: String = ""
    @State private var bookmarkData: Data? = nil
    @State private var errorMessage: String? = nil
    
    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter folder name", text: $folderName)
                //.textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button("Choose Desktop Folder") {
                chooseDesktopFolder()
            }
            
            Button("Create Folder") {
                createFolder()
            }
            .disabled(bookmarkData == nil || folderName.isEmpty)
            
            if let errorMessage = errorMessage {
                Text(errorMessage)
                    .foregroundColor(.red)
            }
        }
        .padding()
        .frame(width: 400, height: 200)
    }
    
    private func chooseDesktopFolder() {
        let openPanel = NSOpenPanel()
        openPanel.canChooseDirectories = true
        openPanel.canChooseFiles = false
        openPanel.allowsMultipleSelection = false
        openPanel.prompt = "Select Folder"
        
        openPanel.begin { response in
            if response == .OK, let selectedURL = openPanel.url {
                do {
                    // Create a bookmark from the selected URL
                    let bookmark = try selectedURL.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
                    self.bookmarkData = bookmark
                    self.errorMessage = "Folder selected successfully."
                } catch {
                    self.errorMessage = "Failed to create bookmark: \(error.localizedDescription)"
                }
            } else {
                self.errorMessage = "No folder selected."
            }
        }
    }
    
    private func createFolder() {
        guard let bookmarkData = bookmarkData else {
            errorMessage = "No folder selected."
            return
        }
        
        do {
            // Resolve the bookmark to get the folder URL
            var isStale = false
            let folderURL = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
            
            // Start accessing the security-scoped resource
            guard folderURL.startAccessingSecurityScopedResource() else {
                errorMessage = "Failed to access folder."
                return
            }
            
            defer { folderURL.stopAccessingSecurityScopedResource() }
            
            // Construct the path of the new folder
            let newFolderURL = folderURL.appendingPathComponent(folderName)
            
            // Use bash command to create the folder
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/bin/mkdir")
            process.arguments = [newFolderURL.path]
            
            try process.run()
            process.waitUntilExit()
            
            if process.terminationStatus == 0 {
                errorMessage = "Folder created successfully."
            } else {
                errorMessage = "Failed to create folder with mkdir command."
            }
        } catch {
            errorMessage = "Error: \(error.localizedDescription)"
        }
    }
}

Solution

  • Since you are accessing the selected files under SIP with write permission, make sure you had enable the read/write permission in the entitlement.

    <dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.files.user-selected.read-write</key>
        <true/>
    </dict>
    

    In Xcode, it's <your-target> -> Signing & Capabilities -> App Sandbox -> File Access, check the option Read/Write for User Selected Files.