Skip to content

Instantly share code, notes, and snippets.

@korylprince
Last active October 31, 2023 02:01
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save korylprince/be2e09e049d2fd721ce769770d983850 to your computer and use it in GitHub Desktop.
Save korylprince/be2e09e049d2fd721ce769770d983850 to your computer and use it in GitHub Desktop.
Modify 10.13 (SFL2) Server Favorites list
#!/usr/local/munki/munki-python
# change the above path to your own python if you don't have Munki installed
"""
Merges add_servers into current favorites and removes remove_servers.
Run as root to update all users or as normal user to update just that user.
"""
import os
import getpass
import subprocess
import uuid
import Foundation
favorites_path = "/Users/{user}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.FavoriteServers.sfl2"
# add these servers if they don't exist already
# use a tuple: ("<name>", "<path>") to set a name for the favorite.
# otherwise just use a string and the path will be used as the name
add_servers = (("My Name", "smb://server.example.com/share"), "vnc://server.example.com")
# remove these servers if they exist. Use a wildcard (*) at the end to remove all shares
# does not support ("<name>", "<path>") syntax
remove_servers = ("smb://old.example.com/*", "vnc://old.example.com")
def get_users():
"Get users with a home directory in /Users"
# get users from dscl
dscl_users = subprocess.check_output(["/usr/bin/dscl", ".", "-list", "/Users"]).splitlines()
# get home directories
homedir_users = os.listdir("/Users")
# return users that are in both lists
users = set(dscl_users).intersection(set(homedir_users))
return [u.strip() for u in users if u.strip() != ""]
def set_favorites(user, add_servers, remove_servers):
"Set the Server Favorites for the given user"
# read existing favorites file
data = Foundation.NSKeyedUnarchiver.unarchiveObjectWithFile_(favorites_path.format(user=user))
# reformat add_servers to [(name, path), ...]
new_add_servers = []
for s in add_servers:
if len(s) == 2:
new_add_servers.append(s)
else:
new_add_servers.append((s, s))
add_servers = new_add_servers
existing_servers = []
# read existing items safely
if data is not None:
data_items = data.get("items", [])
# read existing servers
for item in data_items:
name = item["Name"]
url, _, _ = Foundation.NSURL.initByResolvingBookmarkData_options_relativeToURL_bookmarkDataIsStale_error_(Foundation.NSURL.alloc(), item["Bookmark"], Foundation.NSURLBookmarkResolutionWithoutUI, None, None, None)
existing_servers.append((name, url))
# get unique ordered list of all servers
all_servers = []
# add existing_servers to list, updating name if necessary
for s in existing_servers:
try:
idx = [a[1] for a in add_servers].index(s[1])
all_servers.append((add_servers[idx][0], s[1]))
except ValueError:
all_servers.append(s)
# add add_servers if not present already
for s in add_servers:
if s[1] not in [e[1] for e in existing_servers]:
all_servers.append(s)
# remove old servers: exact match
# matches "smb://old.domain" exactly
all_servers = [s for s in all_servers if s[1] not in remove_servers]
# remove old servers: shares
# matches "smb://old.domain/*"
all_servers = [s for s in all_servers if len(
[True for r in remove_servers if r.endswith("*") and str(s[1]).startswith(r[:-1])]
) < 1]
# generate necessary structures
items = []
for name, path in all_servers:
item = {}
item["Name"] = name
url = Foundation.NSURL.URLWithString_(str(path))
bookmark, _ = url.bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_(0, None, None, None)
item["Bookmark"] = bookmark
# generate a new UUID for each server
item["uuid"] = str(uuid.uuid1()).upper()
item["visibility"] = 0
item["CustomItemProperties"] = Foundation.NSDictionary.new()
items.append(Foundation.NSDictionary.dictionaryWithDictionary_(item))
data = Foundation.NSDictionary.dictionaryWithDictionary_({
"items": Foundation.NSArray.arrayWithArray_(items),
"properties": Foundation.NSDictionary.dictionaryWithDictionary_({"com.apple.LSSharedFileList.ForceTemplateIcons": False})
})
# write sfl2 file
Foundation.NSKeyedArchiver.archiveRootObject_toFile_(data, favorites_path.format(user=user))
# loop through users and set favorites
if __name__ == "__main__":
# if running as root, run for all users. Otherwise run for current user
user = getpass.getuser()
if user == "root":
users = get_users()
else:
users = [user]
for user in users:
try:
set_favorites(user, add_servers, remove_servers)
# fix owner if ran as root
if user == "root":
os.system(("chown {user} " + favorites_path).format(user=user))
print("Server Favorites set for " + user)
except Exception as e:
# if there's an error, log it an continue on
print("Failed setting Server Favorites for {0}: {1}".format(user, str(e)))
# kill sharedfilelistd process to reload file. Finder should be closed when this happens
os.system("killall sharedfilelistd")
#!/usr/local/munki/munki-python
# change the above path to your own python if you don't have Munki installed
"""
Overwrites server favorites with servers.
Run as root to update all users or as normal user to update just that user.
"""
import os
import getpass
import subprocess
import uuid
import Foundation
favorites_path = "/Users/{user}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.FavoriteServers.sfl2"
# Use a tuple: ("<name>", "<path>") to set a name for the favorite.
# Otherwise just use a string and the path will be used as the name
servers = (("My Name", "smb://server.example.com/share"), "vnc://server.example.com")
def get_users():
"Get users with a home directory in /Users"
# get users from dscl
dscl_users = subprocess.check_output(["/usr/bin/dscl", ".", "-list", "/Users"]).splitlines()
# get home directories
homedir_users = os.listdir("/Users")
# return users that are in both lists
users = set(dscl_users).intersection(set(homedir_users))
return [u.strip() for u in users if u.strip() != ""]
def set_favorites(user, servers):
"Set the Server Favorites for the given user"
# generate necessary structures
items = []
for server in servers:
name = server[0] if len(server) == 2 else server
path = server[1] if len(server) == 2 else server
item = {}
item["Name"] = name
url = Foundation.NSURL.URLWithString_(path)
bookmark, _ = url.bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_(0, None, None, None)
item["Bookmark"] = bookmark
# generate a new UUID for each server
item["uuid"] = str(uuid.uuid1()).upper()
item["visibility"] = 0
item["CustomItemProperties"] = Foundation.NSDictionary.new()
items.append(Foundation.NSDictionary.dictionaryWithDictionary_(item))
data = Foundation.NSDictionary.dictionaryWithDictionary_({
"items": Foundation.NSArray.arrayWithArray_(items),
"properties": Foundation.NSDictionary.dictionaryWithDictionary_({"com.apple.LSSharedFileList.ForceTemplateIcons": False})
})
# write sfl2 file
Foundation.NSKeyedArchiver.archiveRootObject_toFile_(data, favorites_path.format(user=user))
# loop through users and set favorites
if __name__ == "__main__":
# if running as root, run for all users. Otherwise run for current user
user = getpass.getuser()
if user == "root":
users = get_users()
else:
users = [user]
for user in users:
try:
set_favorites(user, servers)
# fix owner if ran as root
if user == "root":
os.system(("chown {user} " + favorites_path).format(user=user))
print("Server Favorites set for " + user)
except Exception as e:
# if there's an error, log it an continue on
print("Failed setting Server Favorites for {0}: {1}".format(user, str(e)))
# kill sharedfilelistd process to reload file. Finder should be closed when this happens
os.system("killall sharedfilelistd")
@henningkessler
Copy link

Hello,

does this wonderful script also works with 10.15+?. I am testing this on a 10.15.6 machine and it seams not to work.

Regards

Henning

@korylprince
Copy link
Author

It does, but requires you give permissions to read the files. I put some info on the thread here.

If you wanted to automate this, you'd probably need to use a profile to add those permissions automatically.

@MJGtark
Copy link

MJGtark commented Oct 22, 2021

Hey sorry for such a newb question I have 0 experience with coding or scripts. How/where would I enter the smb locations of the servers in this script. Would it go here where it says servers? servers = (( "smb://server.example.com/share")) Do I just delete the example and put the server addresses here?
I obviously need a course on scripting and python but this seems to do exactly what I am hoping so figured I would ask.

@korylprince
Copy link
Author

Replace the servers = ... line with:

servers = ("smb://server1.example.com", "smb://server2.example.com", "smb://server3.example.com")

replacing the serverX.example.com's with your hostnames.

I have no idea if this works on Big Sur+ or not. Last version I tested it on was Catalina.

@damacguy
Copy link

damacguy commented Mar 17, 2022

Using Monterey, using macadamia's python3 so I updated the first line to match my python3 install. Since python3 uses unicode I replaced all the unicode( with str(, I've got the servers I want added to add_server, and I've commented out remove_servers since I don't need to remove any right now. When I run it chokes at line 26, def get_users(). I don't know enough about python at this point.

@morrkulla
Copy link

Foundation does not appear to be a part of Python3 and older Python seems deprecated in Monterey 12.3.1, what can you use instead of Foundation? Any ideas?

@korylprince
Copy link
Author

I've updated these scripts for Python3. They've been tested on Monterey 12.3.1 with Munki's python. You'll need to point the shebang (first line) to a similarly built Python if you want to continue using these scripts.

The only alternative is if someone writes a compiled CLI tool (e.g. Objective-C, Swift) that uses the same Foundation APIs.

@damacguy
Copy link

Adding servers is working great! Thanks for the updates! But, how does remove_servers work?

@korylprince
Copy link
Author

If you want to remove a server from the existing list, then include it in the remove_servers list"

# remove these servers if they exist. Use a wildcard (*) at the end to remove all shares
# does not support ("<name>", "<path>") syntax
remove_servers = ("smb://old.example.com/*", "vnc://old.example.com")

@damacguy
Copy link

Hate to be a bother... Python is not in my quiver and I'm trying to figure out what I can when I have the time. But on a Monterey machine I'm getting this error now...

Failed setting Server Favorites for adm: 'NoneType' object has no attribute 'bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_'
No matching processes belonging to you were found

I'm using MacAdmin's python with /usr/local/bin/managed_python3 and its obviously getting to this point at least.

@korylprince
Copy link
Author

@damacguy my guess is that you have not used a valid URL in add_servers or remove_servers (for merge.py) or servers (for overwrite.py).

That error is happening because Foundation.NSURL.URLWithString_ is failing to return a parsed URL.

@damacguy
Copy link

Indeed! One of the links the user provided has spaces in it and I didn't think anything of it (facepalm). Thanks for super fast response. 🥇

@vxbush
Copy link

vxbush commented Dec 16, 2022

Q: Have you tested this code on Ventura? I'm assuming it still works because you're using munki's python3. I'm just debating if it is worth it to install another version of python3 just to have the necessary code to make this work.

@korylprince
Copy link
Author

I've not had a chance to test this. I haven't actually used this code in several years, though I've tried to keep it updated for others. I don't see any reason it wouldn't work though, assuming you have a python3 installed.

@damacguy
Copy link

Kory - thanks for maintaining this. Could I ask you a favor? Could you update the script to allow it to accept variables passed to it? I'm not a python guy, but I almost got it working with the macadmin's python3 and taking variables $4 thru $11 to add servers. Maybe $4 could declare if the rest are adding or removing or something. way over my head at this point but I'm working to learn more - got my books and stack overflow.

@hakanmarklund
Copy link

I tried the overwrite.py script in Ventura 13.2 and it worked, but only if not run as root. I found that the reason is that subprocess.check_output() returns the user list as "bytes". I got it working by adding ",encoding='UTF-8'" so that line 26 looks like this:
dscl_users = subprocess.check_output(["/usr/bin/dscl", ".", "-list", "/Users"],encoding='UTF-8').splitlines()

@vn425282
Copy link

may I know how can I change the add_servers pointing to my specific local folder ? let's say I have the ABC folder inside the Documents, and would like to stick it into the SideBar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment