Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
750 views
in Technique[技术] by (71.8m points)

how to read, change, and write macOS file alias from python

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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 the one folder to the Trash and empty the Trash, and only then will a Get Info or resolve of file_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:

#!/usr/bin/env python

# fix_alias.py
# A test program to exercise functionality for retargeting a macOS file alias (bookmark).
# Author: Larry Yaeger, 20 Feb 2018
#
# Create a file and directory hierarchy like the following:
#
# one
#   file.txt
# two
#   file.txt
# file_alias
#
# where one and two are folders, the file.txt files are any files really, and
# file_alias is a Mac file alias that points to ./one/file.txt.  Then run this program
# in the same folder as one, two, and file_alias.  It will replace file_alias with
# an alias that points to ./two/file.txt.
#
# Note that file_alias is NOT a symbolic link, even though the Mac Finder sometimes
# pretends symbolic links are aliases; they are not.

import os
import string

from Foundation import *


OldFolder = 'one'
NewFolder = 'two'
AliasPath = 'file_alias'


def get_bookmarkData(alias_path):
  alias_url = NSURL.fileURLWithPath_(alias_path)
  bookmarkData, error = NSURL.bookmarkDataWithContentsOfURL_error_(alias_url, None)
  return bookmarkData


def get_target_of_bookmarkData(bookmarkData):
  if bookmarkData is None:
    return None
  options = NSURLBookmarkResolutionWithoutUI | NSURLBookmarkResolutionWithoutMounting
  resolved_url, stale, error = 
    NSURL.URLByResolvingBookmarkData_options_relativeToURL_bookmarkDataIsStale_error_(
      bookmarkData, options, None, None, None)
  return resolved_url.path()


def create_bookmarkData(new_path):
  new_url = NSURL.fileURLWithPath_(new_path)
  options = NSURLBookmarkCreationSuitableForBookmarkFile
  new_bookmarkData, error = 
    new_url.bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_(
      options, None, None, None)
  return new_bookmarkData


def create_alias(bookmarkData, alias_path):
  alias_url = NSURL.fileURLWithPath_(alias_path)
  options = NSURLBookmarkCreationSuitableForBookmarkFile
  success, error = NSURL.writeBookmarkData_toURL_options_error_(bookmarkData, alias_url, options, None)
  return success


def main():
  old_bookmarkData = get_bookmarkData(AliasPath)
  old_path = get_target_of_bookmarkData(old_bookmarkData)
  print old_path
  new_path = string.replace(old_path, OldFolder, NewFolder, 1)
  new_bookmarkData = create_bookmarkData(new_path)
  new_path = get_target_of_bookmarkData(new_bookmarkData)
  print new_path
  os.remove(AliasPath)
  create_alias(new_bookmarkData, AliasPath)


main()

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...