Is there any way to read a macOS file alias, modify its contents (particularly the target file path), and write the modified alias back out?
For example, if I have the following directory structure:
./one/file.txt
./two/file.txt
./file_alias
where file_alias
resolves to ./one/file.txt
. I would like to be able to programmatically, in Python, read ./file_alias
, determine its path, change 'one' to 'two', and write the revised alias out, overwriting ./file_alias
. Upon completion, file_alias
would resolve to ./two/file.txt
.
Searching I've found an answer to a related question that suggests it can't be done (@Milliway's answer to [1]), a Carbon module with no substantive documentation and a statement that its functionality has been removed [2], a partially deprecated macostools module that depends on Carbon [3], an equivalent, unanswered question (except a tentative suggestion to use PyObjC) [4], and a recently updated mac_alias package [5], but have not found a way to accomplish the task based on any of these.
The mac_alias package at first seemed interesting, but I have found no way to import the bytes needed to construct an in-memory Alias
object from an existing alias file (using bytes from a binary read of the alias file produces errors) and even if I could construct an in-memory Alias
record and modify it, there is no way to write it out to disk.
The machine where I want this is running 10.12.x (Sierra) and I am using the built-in python 2.7.10. I find I can actually import Carbon and macostools, and suspect Carbon.File might conceivably provide what I need, but I cannot find any documentation for it. I could upgrade to High Sierra and/or install and use Python 3.x, but those don't seem to be helpful or relevant at this stage.
I realize that the alias also contains an inode, that will be stale after such a change, but thankfully, in part due to a bug I filed and a bit of persistence back when I was with Apple, an alias resolves the path first, only falls back to the inode if the path fails to resolve, and updates the inode if the path does resolve (and the inode has changed).
Any help, suggestions, pointers appreciated.
[1] How to handle OSX Aliases in Python with os.walk()?
[2] https://docs.python.org/2/library/carbon.html
[3] https://docs.python.org/2/library/macostools.html
[4] change an alias target python
[5] https://pypi.python.org/pypi/mac_alias
This thread got my interest...
But I don't think it's possible.
Look at this bug report in mac_alias: https://github.com/al45tair/mac_alias/issues/4
it notes that the package handles Alias records not Alias files. The Alias files are a 3rd version which hadn't been reverse engineered yet.
It points to this info on the Alias file: http://indiestack.com/2017/05/resolving-modern-mac-alias-files/
Also this thread on their old bitbucket: https://bitbucket.org/al45tair/mac_alias/issues/3/support-for-version-3-aliases
which points this dead page (thanks, archive.org) https://web.archive.org/web/20170222235430/http://sysforensics.org/2016/08/mac-alias-data-objects/
and info that reading some information is possible via this package: https://pypi.python.org/pypi/plistutils/ which has a bunch of docs on reading alias structures on their github
none of this does what you want though. sorry.
Solved it, using PyObjC, despite there being almost no documentation for PyObjC. You have to carefully convert ObjectiveC interfaces for NSURL to PyObjC calls, using the techniques described in "An Introduction to PyObjC" found on this site while referring to the NSURL interfaces described here.
Code in @MagerValp's reply to this question helped figure out how to get the target of an alias. I had to work out how to create a new alias with a revised target.
Below is a test program that contains and exercises all the functionality needed. Its setup and use are documented with comments in the code.
I'm a bad person and didn't do doc strings or descriptions of inputs and return values, but I've kept all functions short and single-functioned and hopefully I've named all variables and functions sufficiently clearly that they are not needed. There's an admittedly weird combination of CamelCaps and underscore_separated variable and function names. I normally use CamelCaps for global constants and underscore_separated names for functions and variables, but in this case I wanted to keep the variables and data types referred to in the PyObjC calls, which use camelCaps, unchanged, hence the odd mix.
Be warned, the Mac Finder caches some information about aliases. So if you do a Get Info or a resolve on
file_alias
immediately after running this program, it will look like it didn't work, even though it did. You have to drag theone
folder to the Trash and empty the Trash, and only then will a Get Info or resolve offile_alias
show that it does indeed now point to./two/file.txt
. (Grumble, grumble.) Fortunately this will not impact my use of these techniques, nor will it affect most people's use, I suspect. The point of the program will normally be to replace a broken alias with a fixed one, based on the fact that some single, simple thing changed, like the folder name in this example, or the volume name in my real application for this.Finally, the code: