Read and write permission for user selected folder in Mac OS app?
Asked Answered
P

6

14

I am developing MAC OS app which have functionality to create file on the behalf of your. First user select folder for storing file (One time at start of app) and then user can select type and name of the file user want to create on selected folder (Folder selected on start of the app) using apple script. I am able to create file when i add below temporary-exception in entitlement file but its not able to app apple review team but works in sandboxing.

Guideline 2.4.5(i) - Performance We've determined that one or more temporary entitlement exceptions requested for this app are not appropriate and will not be granted:

com.apple.security.temporary-exception.files.home-relative-path.read-write
/FolderName/

I found :

Enabling App Sandbox - Allows apps to write executable files.

And

Enabling User-Selected File Access - Xcode provides a pop-up menu, in the Summary tab of the target editor, with choices to enable read-only or read/write access to files and folders that the user explicitly selects. When you enable user-selected file access, you gain programmatic access to files and folders that the user opens using an NSOpenPanel object, and files the user saves using an NSSavePanel object.

Using below code for creating file :

let str = "Super long string here"
let filename = getDocumentsDirectory().appendingPathComponent("/xyz/output.txt")

do {
    try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
} catch {
    // failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}

Also tried adding com.apple.security.files.user-selected.read-write in entitlement file for an NSOpenPanel object :

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

Is there any way to get pass apple review team to approve Mac App with read and write permission to user selected folder ?

Pampa answered 20/12, 2017 at 9:43 Comment(5)
If the user always selects the target folder (meaning, if your app doesn't try to write without a user action) then you only need com.apple.security.files.user-selected.read-write, nothing else.Hensel
Use the security-scoped bookmark.Plutocracy
@Moritz yeah but i want one time folder selection then every file will be stored in same location.Pampa
Then you still only need com.apple.security.files.user-selected.read-write but you have to use security-scoped bookmark, as El Tomato just said.Hensel
Using security-scoped bookmark. can app ask for folder access permission once and then on button click he can create and save document in same location. @El Tomato please me with some reference sites (except developer.apple.com)Pampa
F
12

Add user-selected and bookmarks.app permissions in entitlement file :

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

Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.

Formica answered 21/12, 2017 at 5:14 Comment(2)
Thanks for the response i will try this.Pampa
How does one add the read-write key, all I see is the Read Only key. I'm using Xcode 14.3 (I'm really new at Xcode and Swift)Prepense
P
22

Here is my Answer How to do implement and persist Read and write permission of user selected folder in Mac OS app?

GitHub Example Project link

First :

Add user-selected and bookmarks.app permissions in entitlement file :

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

Then i created class for all bookmark related function required for storeing, loading ... etc bookmarks app.

import Foundation
import Cocoa

var bookmarks = [URL: Data]()

func openFolderSelection() -> URL?
{
    let openPanel = NSOpenPanel()
    openPanel.allowsMultipleSelection = false
    openPanel.canChooseDirectories = true
    openPanel.canCreateDirectories = true
    openPanel.canChooseFiles = false
    openPanel.begin
        { (result) -> Void in
            if result.rawValue == NSApplication.ModalResponse.OK.rawValue
            {
                let url = openPanel.url
                storeFolderInBookmark(url: url!)
            }
    }
    return openPanel.url
}

func saveBookmarksData()
{
    let path = getBookmarkPath()
    NSKeyedArchiver.archiveRootObject(bookmarks, toFile: path)
}

func storeFolderInBookmark(url: URL)
{
    do
    {
        let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
        bookmarks[url] = data
    }
    catch
    {
        Swift.print ("Error storing bookmarks")
    }

}

func getBookmarkPath() -> String
{
    var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
    url = url.appendingPathComponent("Bookmarks.dict")
    return url.path
}

func loadBookmarks()
{
    let path = getBookmarkPath()
    bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: path) as! [URL: Data]
    for bookmark in bookmarks
    {
        restoreBookmark(bookmark)
    }
}



func restoreBookmark(_ bookmark: (key: URL, value: Data))
{
    let restoredUrl: URL?
    var isStale = false

    Swift.print ("Restoring \(bookmark.key)")
    do
    {
        restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
    }
    catch
    {
        Swift.print ("Error restoring bookmarks")
        restoredUrl = nil
    }

    if let url = restoredUrl
    {
        if isStale
        {
            Swift.print ("URL is stale")
        }
        else
        {
            if !url.startAccessingSecurityScopedResource()
            {
                Swift.print ("Couldn't access: \(url.path)")
            }
        }
    }

}

Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.

To open NSOpenPanel :

let selectedURL = openFolderSelection()
saveBookmarksData()

and to load existing bookmark after app close :

loadBookmarks()

Thats it. I Hope it will help someone.

Pampa answered 23/12, 2017 at 7:2 Comment(3)
Can we define the custom folder read/write access apart from user-selected, movies, pictures etc. OK likeDocumentsNarcotism
Looks interesting Sid but I cannot compile your GitHub project because it crashes in func loadBookmarks().Halleyhalli
Andy, you are right. I edited the code in order to fix that issue. Basically, if bookmarks were never saved then you cannot load them and force unwrap.Hideaway
F
12

Add user-selected and bookmarks.app permissions in entitlement file :

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

Then open folder selection using NSOpenPanel so the user can select which folders to give you access to. The NSOpenPanel must be stored as a bookmark and saved to disk. Then your app will have the same level of access as it did when the user selected the folder.

Formica answered 21/12, 2017 at 5:14 Comment(2)
Thanks for the response i will try this.Pampa
How does one add the read-write key, all I see is the Read Only key. I'm using Xcode 14.3 (I'm really new at Xcode and Swift)Prepense
T
2

Swift 5 with Xcode 14.2 - Jan-2023 :- below code works fine in my macOS app: Keep below code in a class and follow instructions given after the code:

    private static let BOOKMARK_KEY = "bookmark"

// Check permission is granted or not
public static func isPermissionGranted() -> Bool {
    
    if let data = UserDefaults.standard.data(forKey: BOOKMARK_KEY) {
        var bookmarkDataIsStale: ObjCBool = false

        do {
            let url = try (NSURL(resolvingBookmarkData: data, options: [.withoutUI, .withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &bookmarkDataIsStale) as URL)
            if bookmarkDataIsStale.boolValue {
                NSLog("WARNING stale security bookmark")
                return false
            }
            return url.startAccessingSecurityScopedResource()
        } catch {
            print(error.localizedDescription)
            return false
        }
    }
    
    return false
    
} // isPermissionGranted

static func selectFolder(folderPicked: () -> Void) {
    
    let folderChooserPoint = CGPoint(x: 0, y: 0)
    let folderChooserSize = CGSize(width: 450, height: 400)
    let folderChooserRectangle = CGRect(origin: folderChooserPoint, size: folderChooserSize)
    let folderPicker = NSOpenPanel(contentRect: folderChooserRectangle, styleMask: .utilityWindow, backing: .buffered, defer: true)

    let homePath = "/Users/\(NSUserName())"
    folderPicker.directoryURL = NSURL.fileURL(withPath: homePath, isDirectory: true)
    
    folderPicker.canChooseDirectories = true
    folderPicker.canChooseFiles = false
    folderPicker.allowsMultipleSelection = false
    folderPicker.canDownloadUbiquitousContents = false
    folderPicker.canResolveUbiquitousConflicts = false
    
    folderPicker.begin { response in
        
        if response == .OK {
            let url = folderPicker.urls
            NSLog("\(url)")
            
            // Save Url Bookmark
            if let mUrl = folderPicker.url {
                storeFolderInBookmark(url: mUrl)
            }
            
        }
    }
    
}

private static func storeFolderInBookmark(url: URL) { // mark 1
    do {
        let data = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
        UserDefaults.standard.set(data, forKey: BOOKMARK_KEY)
    } catch {
        NSLog("Error storing bookmarks")
    }
}

How to Use:

isPermissionGranted() - this function is to check user has granted directory permission or not. If it returns true then use directory/file operation read/write. If it returns false then use call selectFolder() function

selectFolder() - if isPermissionGranted() returns false then call this function to take permission from user. user will just need to click on as home directory will choose automatically.

storeFolderInBookmark() - Just keep it in the code you don't need to modify it, it will save url as bookmark for future use

Hope this will help & save lots of time. Thanks.

Taunt answered 29/1, 2023 at 10:28 Comment(0)
N
1

Since 'unarchiveObject(withFile:)' was deprecated in macOS 10.14, created a new answer in case someone has a similar question.

So after setting this in plist,

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

Create a BookMark class like below:

import Foundation

@objcMembers final class BookMarks: NSObject, NSSecureCoding {
    struct Keys {
        static let data = "data"
    }
    
    var data: [URL:Data] = [URL: Data]()
    
    static var supportsSecureCoding: Bool = true
    
    required init?(coder: NSCoder) {
        self.data = coder.decodeObject(of: [NSDictionary.self, NSData.self, NSURL.self], forKey: Keys.data) as? [URL: Data] ?? [:]
    }
    
    required init(data: [URL: Data]) {
        self.data = data
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(data, forKey: Keys.data)
    }
    
    func store(url: URL) {
        do {
            let bookmark = try url.bookmarkData(options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
            data[url] = bookmark
        } catch {
            print("Error storing bookmarks")
        }
    }
    
    func dump() {
        let path = Self.path()
        do {
            try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true).write(to: path)
        } catch {
            print("Error dumping bookmarks")
        }
    }
    
    static func path() -> URL {
        var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
        url = url.appendingPathComponent("Bookmarks.dict")
        return url
    }
    
    static func restore() -> BookMarks? {
        let path = Self.path()
        let nsdata = NSData(contentsOf: path)
        
        guard nsdata != nil else { return nil }
        
        do {
            let bookmarks = try NSKeyedUnarchiver.unarchivedObject(ofClass: Self.self, from: nsdata! as Data)
            for bookmark in bookmarks?.data ?? [:] {
                Self.restore(bookmark)
            }
            return bookmarks
        } catch {
            // print(error.localizedDescription)
            print("Error loading bookmarks")
            return nil
        }
    }
    
    static func restore(_ bookmark: (key: URL, value: Data)) {
        let restoredUrl: URL?
        var isStale = false
        
        print("Restoring \(bookmark.key)")
        do {
            restoredUrl = try URL.init(resolvingBookmarkData: bookmark.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
        } catch {
            print("Error restoring bookmarks")
            restoredUrl = nil
        }
        
        if let url = restoredUrl {
            if isStale {
                print("URL is stale")
            } else {
                if !url.startAccessingSecurityScopedResource() {
                    print("Couldn't access: \(url.path)")
                }
            }
        }
    }
}

Then using it:

  1. loading
let bookmarks = BookMarks.restore() ?? BookMarks(data: [:])
  1. adding
bookmarks.store(url: someUrl)
  1. saving
bookmarks.dump()
Newfoundland answered 18/4, 2022 at 16:29 Comment(1)
Works like a charm! Thank you! I have been trying to figure this out all day!Deferent
B
0

I found the best and working answer here - reusing security scoped bookmark

Super simple, easy to understand and does the job pretty well.

The solution was :-

var userDefault = NSUserDefaults.standardUserDefaults()
var folderPath: NSURL? {
    didSet {
        do {
            let bookmark = try folderPath?.bookmarkDataWithOptions(.SecurityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeToURL: nil)
            userDefault.setObject(bookmark, forKey: "bookmark")
        } catch let error as NSError {
            print("Set Bookmark Fails: \(error.description)")
        }
    }
}

func applicationDidFinishLaunching(aNotification: NSNotification) {
    if let bookmarkData = userDefault.objectForKey("bookmark") as? NSData {
        do {
            let url = try NSURL.init(byResolvingBookmarkData: bookmarkData, options: .WithoutUI, relativeToURL: nil, bookmarkDataIsStale: nil)
            url.startAccessingSecurityScopedResource()
        } catch let error as NSError {
            print("Bookmark Access Fails: \(error.description)")
        }
    }
}
Breadfruit answered 12/11, 2018 at 12:5 Comment(0)
F
0

Updated to Swift 5 (Thanks Jay!)

var folderPath: URL? {
    didSet {
        do {
            let bookmark = try folderPath?.bookmarkData(options: .securityScopeAllowOnlyReadAccess, includingResourceValuesForKeys: nil, relativeTo: nil)
            UserDefaults.standard.set(bookmark, forKey: "bookmark")
        } catch let error as NSError {
            print("Set Bookmark Fails: \(error.description)")
        }
    }
}

 func applicationDidFinishLaunching(_ aNotification: Notification) {
        if let bookmarkData = UserDefaults.standard.object(forKey: "bookmark") as? Data {
            do {
                var bookmarkIsStale = false
                let url = try URL.init(resolvingBookmarkData: bookmarkData as Data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &bookmarkIsStale)
                url.startAccessingSecurityScopedResource()
            } catch let error as NSError {
                print("Bookmark Access Fails: \(error.description)")
            }
        }
    }
Fou answered 9/1, 2021 at 15:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.