Getting alias path of file in swift - macos

I'm having trouble resolving the alias link on mac. I'm checking if the file is an alias and then I would want to receive the original path. Instead I'm only getting a File-Id.
Anly ideas?
func isFinderAlias(path:String) -> Bool? {
var isAlias:Bool? = false // Initialize result var.
// Create a CFURL instance for the given filesystem path.
// This should never fail, because the existence isn't verified at this point.
// Note: No need to call CFRelease(fUrl) later, because Swift auto-memory-manages CoreFoundation objects.
print("path before \(path)");
let fUrl = CFURLCreateWithFileSystemPath(nil, path, CFURLPathStyle.CFURLPOSIXPathStyle, false)
print("path furl \(fUrl)");
// Allocate void pointer - no need for initialization,
// it will be assigned to by CFURLCopyResourcePropertyForKey() below.
let ptrPropVal = UnsafeMutablePointer<Void>.alloc(1)
// Call the CoreFoundation function that copies the desired information as
// a CFBoolean to newly allocated memory that prt will point to on return.
if CFURLCopyResourcePropertyForKey(fUrl, kCFURLIsAliasFileKey, ptrPropVal, nil) {
// Extract the Bool value from the memory allocated.
isAlias = UnsafePointer<CFBoolean>(ptrPropVal).memory as Bool
// it will be assigned to by CFURLCopyResourcePropertyForKey() below.
let ptrDarwin = UnsafeMutablePointer<DarwinBoolean>.alloc(1)
if ((isAlias) == true){
if let bookmark = CFURLCreateBookmarkDataFromFile(kCFAllocatorDefault, fUrl, nil){
let url = CFURLCreateByResolvingBookmarkData(kCFAllocatorDefault, bookmark.takeRetainedValue(), CFURLBookmarkResolutionOptions.CFBookmarkResolutionWithoutMountingMask, nil, nil, ptrDarwin, nil)
print("getting the path \(url)")
}
}
// Since the CF*() call contains the word "Copy", WE are responsible
// for destroying (freeing) the memory.
ptrDarwin.destroy()
ptrDarwin.dealloc(1)
ptrPropVal.destroy()
}
// Deallocate the pointer
ptrPropVal.dealloc(1)
return isAlias
}
EDIT:
Both Answers are correct!
I would choose the answer of mklement0 due to the originally not stated requirement that the code run on 10.9 which makes it more flexible

This is a solution using NSURL.
It expects an NSURL object as parameter and returns either the original path if the url is an alias or nil.
func resolveFinderAlias(url:NSURL) -> String? {
var isAlias : AnyObject?
do {
try url.getResourceValue(&isAlias, forKey: NSURLIsAliasFileKey)
if isAlias as! Bool {
do {
let original = try NSURL(byResolvingAliasFileAtURL: url, options: NSURLBookmarkResolutionOptions())
return original.path!
} catch let error as NSError {
print(error)
}
}
} catch _ {}
return nil
}
Swift 3:
func resolveFinderAlias(at url: URL) -> String? {
do {
let resourceValues = try url.resourceValues(forKeys: [.isAliasFileKey])
if resourceValues.isAliasFile! {
let original = try URL(resolvingAliasFileAt: url)
return original.path
}
} catch {
print(error)
}
return nil
}
Be aware to provide appropriate entitlements if the function is called in a sandboxed environment.

vadian's answer works great on OS X 10.10+.
Here's an implementation that also works on OS X 10.9:
// OSX 10.9+
// Resolves a Finder alias to its full target path.
// If the given path is not a Finder alias, its *own* full path is returned.
// If the input path doesn't exist or any other error occurs, nil is returned.
func resolveFinderAlias(path: String) -> String? {
let fUrl = NSURL(fileURLWithPath: path)
var targetPath:String? = nil
if (fUrl.fileReferenceURL() != nil) { // item exists
do {
// Get information about the file alias.
// If the file is not an alias files, an exception is thrown
// and execution continues in the catch clause.
let data = try NSURL.bookmarkDataWithContentsOfURL(fUrl)
// NSURLPathKey contains the target path.
let rv = NSURL.resourceValuesForKeys([ NSURLPathKey ], fromBookmarkData: data)
targetPath = rv![NSURLPathKey] as! String?
} catch {
// We know that the input path exists, but treating it as an alias
// file failed, so we assume it's not an alias file and return its
// *own* full path.
targetPath = fUrl.path
}
}
return targetPath
}
Note:
Unlike vadian's solution, this will return a value even for non-alias files, namely that file's own full path, and takes a path string rather than a NSURL instance as input.
vadian's solution requires appropriate entitlements in order to use the function in a sandboxed application/environment. It seems that this one at least doesn't need that to the same extent, as it will run in an Xcode Playground, unlike vadian's solution. If someone can shed light on this, please help.
Either solution, however, does run in a shell script with shebang line #!/usr/bin/env swift.
If you want to explicitly test whether a given path is a Finder alias, see this answer, which is derived from vadian's, but due to its narrower focus also runs on 10.9.

Here's a Swift 3 implementation, based largely on vadian's approach. My idea is to return a file URL, so I effectively combine it with fileURLWithPath. It's an NSURL class extension because I need to be able to call into it from existing Objective-C code:
extension NSURL {
class func fileURL(path:String, resolveAlias yn:Bool) -> URL {
let url = URL(fileURLWithPath: path)
if !yn {
return url
}
do {
let vals = try url.resourceValues(forKeys: [.isAliasFileKey])
if let isAlias = vals.isAliasFile {
if isAlias {
let original = try URL(resolvingAliasFileAt: url)
return original
}
}
} catch {
return url // give up
}
return url // really give up
}
}

URL variant I need to return nil (not an alias or error) else original - Swift4
func resolvedFinderAlias() -> URL? {
if (self.fileReferenceURL() != nil) { // item exists
do {
// Get information about the file alias.
// If the file is not an alias files, an exception is thrown
// and execution continues in the catch clause.
let data = try NSURL.bookmarkData(withContentsOf: self as URL)
// NSURLPathKey contains the target path.
let rv = NSURL.resourceValues(forKeys: [ URLResourceKey.pathKey ], fromBookmarkData: data)
var urlString = rv![URLResourceKey.pathKey] as! String
if !urlString.hasPrefix("file://") {
urlString = "file://" + urlString
}
return URL.init(string: urlString)
} catch {
// We know that the input path exists, but treating it as an alias
// file failed, so we assume it's not an alias file so return nil.
return nil
}
}
return nil
}

Related

How to save security scoped URL for later use macOS

I've made a Finder extension to add a menu to Finder's Context menu for any file. I'd like to access this file when the user selects this custom menu, obviously this file they select could be anywhere in the file system and outside the allowed sandbox areas.
func accessFile(url: URL, userID: String, completion: #escaping ([String:Any]?, Error?) -> Void){
var bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
print("Testing if we have access to file")
// 1. Test if I have access to a file
let directoryURL = url.deletingLastPathComponent()
let data = bookmarks?[directoryURL]
if data == nil{
print("have not asked for access yet or directory is not saved")
// 2. If I do not, open a open dialog, and get permission
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = false
openPanel.prompt = "Grant Access"
openPanel.directoryURL = directoryURL
openPanel.begin { result in
guard result == .OK, let url = openPanel.url else {return}
// 3. obtain bookmark data of folder URL and store it to keyed archive
do{
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
}catch{
print(error)
}
bookmarks?[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// 4. start using the fileURL via:
url.startAccessingSecurityScopedResource()
// < do whatever to file >
url.stopAccessingSecurityScopedResource()
}
}else{
// We have accessed this directory before, get data from bookmarks
print("we have access already")
let directoryURL = url.deletingLastPathComponent()
guard let data = bookmarks?[directoryURL]! else { return }
var isStale = false
let newURL = try? URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
// 3. Now again I start using file URL and upload:
newURL?.startAccessingSecurityScopedResource()
// < do whatever to file >
newURL?.stopAccessingSecurityScopedResource()
}
}
Currently it always asks for permission, so the bookmark is not getting saved
I'm not 100% sure if this is the source of your problem, but I don't see where you are using the isStale value. If it it comes back true from URL(resolvingBookmarkData:...), you have to remake/resave the bookmark. So in your else block you need some code like this:
var isStale = false
let newURL = try? URL(
resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
if let url = newURL, isStale
{
do
{
data = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
}
catch { fatalError("Remaking bookmark failed") }
// Resave the bookmark
bookmarks?[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
}
newURL?.startAccessingSecurityScopedResource()
// < do whatever to file >
newURL?.stopAccessingSecurityScopedResource()
data will, of course, need to be var instead of let now.
Also remember that stopAccessingSecurityScopedResource() has to be called on main thread, so if you're not sure accessFile is being called on the main thread, you might want to do that explicitly:
DispatchQueue.main.async {
newURL?.stopAccessingSecurityScopedResource()
}
You'd want to do that in both places you call it.
I like to write an extension on URL to make it a little nicer:
extension URL
{
func withSecurityScopedAccess<R>(code: (URL) throws -> R) rethrows -> R
{
self.startAccessingSecurityScopedResource()
defer {
DispatchQueue.main.async {
self.stopAccessingSecurityScopedResource()
}
}
return try code(self)
}
}
So then I can write:
url.withSecurityScopedAccess { url in
// Do whatever with url
}
Whether you use the extension or not, explicitly calling stopAccessingSecurityScopedResource() on DispatchQueue.main does mean that access won't be stopped until the next main run loop iteration. That's normally not a problem, but if you start and stop the access for the same URL multiple times in a single run loop iteration, it might not work, because it will call startAccessingSecurityScopedResource() multiple time without stopAccessingSecurityScopedResource() in between, and the on the next iteration it would call stopAccessingSecurityScopedResource() multiple times as the queued tasks are executed. I have no idea if URL maintains a security access count that would allow that to be safe, or just a flag, in which case it wouldn't be.
Let's make some issues visible by removing the bookmark and NSOPenPanel code:
var bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
// bookmarks is an optional and can be nil (when the file doesn't exist)
let data = bookmarks?[directoryURL]
if data == nil {
// NSOpenPanel
do {
let data = try openPanelUrl.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
// this data is a local data, the other data didn't change
} catch {
print(error)
}
// bookmarks and data are still nil
bookmarks?[openPanelUrl] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// use url
} else {
// get the bookmark data again
guard let data = bookmarks?[directoryURL]! else { return }
// get url from data and use it
}
I would do something like:
var bookmarks: [URL: Data]
if let savedBookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data] {
bookmarks = savedBookmarks
}
else {
bookmarks = [:]
}
// bookmarks is a dictionary and can be saved
if let data = bookmarks[directoryURL] {
// get url from data and use it
}
else {
// NSOpenPanel
do {
if let newData = try openPanelUrl.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) {
// bookmarks and newData are not nil
bookmarks[openPanelUrl] = newData
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// use url
}
} catch {
print(error)
}
}

is anyone able to restrict the type of the objects dropped on the mac in SwiftUI 3?

as per the documentation, it should be pretty straightforward. example for a List: https://developer.apple.com/documentation/swiftui/list/ondrop(of:istargeted:perform:)-75hvy#
the UTType should be the parameter restricting what a SwiftUI object can receive. in my case i want to accept only Apps. the UTType is .applicationBundle: https://developer.apple.com/documentation/uniformtypeidentifiers/uttype/3551459-applicationbundle
but it doesn't work. the SwiftUI object never changes status and never accepts the drop. the closure is never run. whether on Lists, H/VStacks, Buttons, whatever. the pdf type don't seem to work either, as well as many others. the only type that i'm able to use if fileURL, which is mainly like no restriction.
i'm not sure if i'm doing something wrong or if SwiftUI is half working for the mac.
here's the code:
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.applicationBundle, .application], isTargeted: isTargeted) { providers in
print("hehe")
return true
}
replacing or just adding .fileURL in the UTType array makes the drop work but without any type restriction.
i've also tried to use .onInsert on a ForEach instead (https://developer.apple.com/documentation/swiftui/foreach/oninsert(of:perform:)-2whxl#), and to go through a proper DropDelegate (https://developer.apple.com/documentation/swiftui/dropdelegate#) but keep getting the same results. it would seem the SwiftUI drop for macOS is not yet working, but i can't find any official information about this. in the docs it is written macOS 11.0+ so i would expect it to work?
any info appreciated! thanks.
You need to validate manually, using DropDelegate of what kind of file is dragged over.
Here is a simplified demo of possible approach. Tested with Xcode 13 / macOS 11.6
let delegate = MyDelegate()
...
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.fileURL], delegate: delegate) // << accept file URLs
and verification part like
class MyDelegate: DropDelegate {
func validateDrop(info: DropInfo) -> Bool {
// find provider with file URL
guard info.hasItemsConforming(to: [.fileURL]) else { return false }
guard let provider = info.itemProviders(for: [.fileURL]).first else { return false }
var result = false
if provider.canLoadObject(ofClass: String.self) {
let group = DispatchGroup()
group.enter() // << make decoding sync
// decode URL from item provider
_ = provider.loadObject(ofClass: String.self) { value, _ in
defer { group.leave() }
guard let fileURL = value, let url = URL(string: fileURL) else { return }
// verify type of content by URL
let flag = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle
result = flag ?? false
}
// wait a bit for verification result
_ = group.wait(timeout: .now() + 0.5)
}
return result
}
func performDrop(info: DropInfo) -> Bool {
// handling code is here
return true
}
}

Sandbox & WKWebView loadFileURL(_, allowingReadAccessTo:) Inconsistency

Using the new shiny WKWebView and sandbox on os/x, require some intervening reset or clear as subsequent calls to load a file URL will be ignored; this is somewhat related to an earlier question on WKWebView loadFileURL works only once -
ios there, here on os/X I do
if loadURL.isFileURL {
webView.loadFileURL(loadURL, allowingReadAccessTo: loadURL)
}
else
{
webView.load(URLRequest(url: loadURL))
}
I've tried to pass loadURL.deletingLastPathComponent() as the second arg but then all breaks - no file URLs get loaded, nor does using the user's home path, or the entire root 'file:///', nor the 'temporary' exception re: absolute file paths. Finally, trying an intervening topLoading() has no affect.
The only solution (yuck) to get a subsequent file URL loaded is to first load a non file URL!
It seems within a sandbox environment this has unintended consequences?
Well, this works but ugly - webView subclass function, as you cannot reuse a webView when a file url was previously loaded. This workaround will instantiate a new window/doc tossing the old - unless as a user preference they want to keep the old window (newWindows flag is true):
func loadNext(url: URL) {
let doc = self.window?.windowController?.document as! Document
let newWindows = UserSettings.createNewWindows.value
var fileURL = url
if !url.isFileURL {
if newWindows {
do
{
let next = try NSDocumentController.shared().openUntitledDocumentAndDisplay(true) as! Document
let oldWindow = self.window
let newWindow = next.windowControllers.first?.window
(newWindow?.contentView?.subviews.first as! MyWebView).load(URLRequest(url: url))
newWindow?.offsetFromWindow(oldWindow!)
}
catch let error {
NSApp.presentError(error)
Swift.print("Yoink, unable to create new url doc for (\(url))")
return
}
}
else
{
self.load(URLRequest(url: url))
}
}
if let origURL = (fileURL as NSURL).resolvedFinderAlias() {
fileURL = origURL
}
if appDelegate.isSandboxed() && !appDelegate.storeBookmark(url: fileURL) {
Swift.print("Yoink, unable to sandbox \(fileURL))")
return
}
if !(self.url?.isFileURL)! && !newWindows {
self.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
doc.update(to: fileURL, ofType: fileURL.pathExtension)
return
}
// We need or want a new window; if need, remove the old afterward
do {
let next = try NSDocumentController.shared().openUntitledDocumentAndDisplay(true) as! Document
let oldWindow = doc.windowControllers.first?.window
let newWindow = next.windowControllers.first?.window
(newWindow?.contentView?.subviews.first as! MyWebView).loadFileURL(fileURL, allowingReadAccessTo: fileURL)
if newWindows {
newWindow?.offsetFromWindow(oldWindow!)
}
else
{
newWindow?.overlayWindow(oldWindow!)
oldWindow?.orderOut(self)
}
next.update(to: fileURL, ofType: fileURL.pathExtension)
}
catch let error
{
NSApp.presentError(error)
Swift.print("Yoink, unable to new doc (\(fileURL))")
}
}

xCode playground and writing files to Documents

I'm trying to test some sqlite database calls through XCode's playground. I start with a database in my Playground's Resources folder and try to move it to the Playgrounds Documents folder, however what happens is that a symbolic link is generated pointing back to the file within the Resources folder so I am unable to write to that file. However, If I figure out where the Documents folder is and then copy the file there by hand from the terminal everything works just fine.
So why does the file manager copy command actually create a sym link to rather than copy? And is there any way to actually make this happen? It seems to only be a problem with the Playground. copy from Resource to Documents works fine in the app itself.
some code to test within the playground...
let dirPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask, true)
let docsDir = dirPaths[0]
let destPath = (docsDir as NSString).appendingPathComponent("/data.sqlite")
print(destPath)
let fileMgr = FileManager.default
let srcPath = Bundle.main.path(forResource: "data", ofType:"sqlite")
// This copies the data.sqlite file from Resources to Documents
// ** However in the playground, only a symlink is generated
do {
try fileMgr.copyItem(atPath: srcPath!, toPath: destPath)
} catch let error {
print("Error (during copy): \(error.localizedDescription)")
}
Rod, It's too late but, I figure this out.
Context:
I add one playground to my project in the same workspace
To make the playground works with my project, I create a framework
I added the Store.db file to the framework target
I haven't added the Store.db to the Playground as a Resource
Swift 3
Playground
#testable import MyAppAsFramework
func copyMockDBToDocumentsFolder(dbPath: String) {
let localDBName = "Store.db"
let documentPath = dbPath / localDBName
let dbFile = Bundle(for: MyAppAsFrameworkSomeClass.self).path(forResource: localDBName.deletingPathExtension, ofType: localDBName.pathExtension)
FileManager.copyFile(dbFile, toFolderPath: documentPath)
}
copyMockDBToDocumentsFolder(dbPath: "The documents path")
Things like / copyFile(x) and the others are operator overloads and extensions
extension String {
public var pathExtension: String {
get {
return (self as NSString).pathExtension
}
}
public var deletingPathExtension: String {
get {
return (self as NSString).deletingPathExtension
}
}
public func appendingPath(_ path: String) -> String {
let nsSt = self as NSString
return nsSt.appendingPathComponent(path)
}
}
infix operator / : MultiplicationPrecedence
public func / (left: String, right: String) -> String {
return left.appendingPath(right)
}
extension FileManager {
class func copyFile(_ filePath: String?, toFolderPath: String?) -> Bool {
guard let file = filePath else { return false }
guard let toFolder = toFolderPath else { return false }
var posibleError: NSError?
do {
try FileManager.default.copyItem(atPath: file, toPath:toFolder)
} catch let error as NSError {
posibleError = error
print("CAN'T COPY \(error.localizedDescription)")
}
return posibleError == nil
}
}

Check if file is alias Swift

How can I check if a file is an Alias on Mac? Here is my code so far:
public func getFiles(){
let folderPath = "/Users/timeBro/Desktop/testfolder"
let fileManager = NSFileManager.defaultManager()
let enumerator:NSDirectoryEnumerator = fileManager.enumeratorAtPath(folderPath)!
for url in enumerator.allObjects {
let newurl = NSURL(string: url as! String)
print("\(url as! String)")
print(url);
print(newurl?.isFileReferenceURL())
}
}
How can I check if the file is and alias?
There is an easy solution which completely gets by without any pointer handling:
extension URL {
func isAlias() -> Bool? {
let values = try? url.resourceValues(forKeys: [.isSymbolicLinkKey, .isAliasFileKey])
let alias = values?.isAliasFile
let symbolic = values?.isSymbolicLink
guard alias != nil, symbolic != nil else { return nil }
if alias! && !symbolic! {
return true
}
return false
}
}
Explanation: resourceValues(forKeys:) returns .isAliasFile and .isSymbolicLink for symbolic links so you have to make sure the former is returned and the latter isn’t when checking for aliases.
If the path doesn’t exist the function returns nil.
Update: I initially mistakenly assumed that the only option is to use a CoreFoundation (C API) method, but that's not actually true (thanks): the Foundation (ObjC API) class NSURL does provide a way to detect Finder aliases:
// OSX 10.9+
// Indicates if the specified filesystem path is a Finder alias.
// Returns an optional Boolean: if the lookup failed, such when the path doesn't exist,
// nil is returned.
// Example: isFinderAlias("/path/to/an/alias")
func isFinderAlias(path:String) -> Bool? {
let aliasUrl = NSURL(fileURLWithPath: path)
var isAlias:AnyObject? = nil
do {
try aliasUrl.getResourceValue(&isAlias, forKey: NSURLIsAliasFileKey)
} catch _ {}
return isAlias as! Bool?
}
[Not recommended, except as an exercise in using UnsafeMutablePointer<Void>]
Here's how to do it with the C-based CoreFoundation API:
IsAliasFile() was deprecated in OS X 10.4,
succeeded by FSIsAliasFile(), which was deprecated in 10.8.
The current method is to use CFURLCopyResourcePropertyForKey(), which isn't fun to deal with in Swift, due to having to use manual memory management with UnsafeMutablePointer<Void>.
I hope I got the memory management right:
import Foundation
// Indicates if the specified filesystem path is a Finder alias.
// Returns an optional Boolean: if the lookup failed, such when the path
// doesn't exist, nil is returned.
// Example: isFinderAlias("/path/to/an/alias")
func isFinderAlias(path:String) -> Bool? {
var isAlias:Bool? = nil // Initialize result var.
// Create a CFURL instance for the given filesystem path.
// This should never fail, because the existence isn't verified at this point.
// Note: No need to call CFRelease(fUrl) later, because Swift auto-memory-manages CoreFoundation objects.
let fUrl = CFURLCreateWithFileSystemPath(nil, path, CFURLPathStyle.CFURLPOSIXPathStyle, false)
// Allocate void pointer - no need for initialization,
// it will be assigned to by CFURLCopyResourcePropertyForKey() below.
let ptrPropVal = UnsafeMutablePointer<Void>.alloc(1)
// Call the CoreFoundation function that copies the desired information as
// a CFBoolean to newly allocated memory that prt will point to on return.
if CFURLCopyResourcePropertyForKey(fUrl, kCFURLIsAliasFileKey, ptrPropVal, nil) {
// Extract the Bool value from the memory allocated.
isAlias = UnsafePointer<CFBoolean>(ptrPropVal).memory as Bool
// Since the CF*() call contains the word "Copy", WE are responsible
// for destroying (freeing) the memory.
ptrPropVal.destroy()
}
// Deallocate the pointer
ptrPropVal.dealloc(1)
return isAlias
}

Resources