osxmetadata: Setting `kMDItemWhereFroms` doesn't show in Finder

This doesn’t cause the URL to show in Finder:

import plistlib
from osxmetadata import *

url = "https://apple.com"
out_path = "/tmp/test_md.txt"

with open(out_path, "w") as f:
    f.write("hi")

meta = OSXMetaData(out_path)
meta.update_attribute(kMDItemWhereFroms, [url])

The attribute also is not listed with mdls:

$ mdls /tmp/test_md.txt
kMDItemFSContentChangeDate = 2022-09-20 14:56:00 +0000
kMDItemFSCreationDate      = 2022-09-20 14:56:00 +0000
kMDItemFSCreatorCode       = ""
kMDItemFSFinderFlags       = 0
kMDItemFSHasCustomIcon     = 0
kMDItemFSInvisible         = 0
kMDItemFSIsExtensionHidden = 0
kMDItemFSIsStationery      = 0
kMDItemFSLabel             = 0
kMDItemFSName              = "testmd.txt"
kMDItemFSNodeCount         = 2
kMDItemFSOwnerGroupID      = 0
kMDItemFSOwnerUserID       = 501
kMDItemFSSize              = 2
kMDItemFSTypeCode          = ""

But it IS there, and looks to be correctly formatted as a binary plist:

$ xattr -l /tmp/test_md.txt
com.apple.metadata:kMDItemWhereFroms: bplist00�_https://apple.com

I’m on Monterey 12.6 (21G115)

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 21 (13 by maintainers)

Commits related to this issue

Most upvoted comments

I’ll add a note to the docs about temporary files. I got the test suite running last night in GitHub actions (via a BigSur VM, the latest available in GitHub). Interestingly the same three tests fail with the same result. Something about kMDItemAuthors isn’t right for macOS > Catalina. I’ll open a separate issue for this.

tests/test_cli.py:517: AssertionError
=========================== short test summary info ============================
FAILED tests/test_cli.py::test_cli_remove - AssertionError: assert ['John Doe...
FAILED tests/test_cli.py::test_cli_backup_restore - AssertionError: assert no...
FAILED tests/test_cli.py::test_cli_order - AssertionError: assert ['John Doe'...
=================== 3 failed, 551 passed, 3 skipped in 8.14s ===================

Whoa, what a bizarre quirk! Indeed, when I write something to my home directory instead, the Where froms are set as expected and are shown immediately in Finder. Still doesn’t explain the test errors…

But maybe this can be closed after all? Probably a good idea to document this /tmp quirk as well. Thanks so much!

I’m on Monterey 12.6. Hopefully it’s not an OS version issue… but I’ll give the tests a run and report back.

@nk9 would you mind cloning the repo then running the test suite? See instructions in README_DEV.md for how to install/build the package.

@nk9 I’ve release version 1.0.0 of osxmetadata that fixes this bug and several others. It’s a complete rewrite to use the native macOS calls to get/set metadata. It does change the API in breaking ways though so check out the README.md.

This is incredible, thank you! I was thinking that I should try using MDItemSetAttribute as used in the browser code above, but hadn’t gotten around to it since I thought it would have to be in ObjC or Swift. Kudos for working it out, and so quickly!

@nk9 I’ve figured out how to call the undocumented MDItemSetAttribute from python. The following snippet (also as a gist) will set kMDItemWhereFroms if called like this:

python setmd.py file.txt kMDItemWhereFroms array google.com

"""Set metadata on macOS files using undocumented function MDItemSetAttribute

Background: Apple provides MDItemCopyAttribute to get metadata from files:
https://developer.apple.com/documentation/coreservices/1427080-mditemcopyattribute?language=objc

but does not provide a documented way to set file metadata.

This script shows how to use the undocumented function MDItemSetAttribute to do so.

`pip install pyobjc` to install the required Python<-->Objective C bridge package.
"""

import sys
from typing import List, Union

import CoreFoundation
import CoreServices
import objc

# load undocumented function MDItemSetAttribute
# signature: Boolean MDItemSetAttribute(MDItemRef, CFStringRef name, CFTypeRef attr);
# references:
# https://github.com/WebKit/WebKit/blob/5b8ad34f804c64c944ebe43c02aba88482c2afa8/Source/WTF/wtf/mac/FileSystemMac.MDItemSetAttribute
# https://pyobjc.readthedocs.io/en/latest/metadata/manual.html#objc.loadBundleFunctions
# signature of B@@@ translates to returns BOOL, takes 3 arguments, all objects
# In reality, the function takes references (pointers) to the objects, but pyobjc barfs if
# the function signature is specified using pointers.
# Specifying generic objects allows the bridge to convert the Python objects to the
# appropriate Objective C object pointers.


def MDItemSetAttribute(mditem, name, attr):
    """dummy function definition"""
    ...


# This will load MDItemSetAttribute from the CoreServices framework into module globals
objc.loadBundleFunctions(
    CoreServices.__bundle__,
    globals(),
    [("MDItemSetAttribute", b"B@@@")],
)


def set_file_metadata(file: str, attribute: str, value: Union[str, List]) -> bool:
    """Set file metadata using undocumented function MDItemSetAttribute

    file: path to file
    attribute: metadata attribute to set
    value: value to set attribute to; must match the type expected by the attribute (e.g. str or list)

    Note: date attributes (e.g. kMDItemContentCreationDate) not yet handled.

    Returns True if successful, False otherwise.
    """
    mditem = CoreServices.MDItemCreate(None, file)
    if isinstance(value, list):
        value = CoreFoundation.CFArrayCreate(
            None, value, len(value), CoreFoundation.kCFTypeArrayCallBacks
        )
    return MDItemSetAttribute(
        mditem,
        attribute,
        value,
    )


def main():
    """Set metadata on macOS files using undocumented function MDItemSetAttribute

    Usage: setmd.py <file> <attribute> <type> <value> <value> ...

    <file>: path to file
    <attribute>: metadata attribute to set, e.g. kMDItemWhereFroms
    <type>: type of value to set, e.g. string or array; must match the type expected by the attribute (e.g. str or list)
    <value>: value(s) to set attribute to

    For example: setmd.py /tmp/test.txt kMDItemWhereFroms array http://example.com

    For metadata attributes and types, see https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys?language=objc
    """
    # super simple argument parsing just for demo purposes
    if len(sys.argv) < 5:
        print(main.__doc__)
        sys.exit(1)

    file = sys.argv[1]
    attribute = sys.argv[2]
    type_ = sys.argv[3]
    values = sys.argv[4:]

    if type_ == "string":
        values = values[0]

    try:
        attribute = getattr(CoreServices, attribute)
    except AttributeError:
        print(f"Invalid attribute: {attribute}")
        sys.exit(1)

    if not set_file_metadata(file, attribute, values):
        print(f"Failed to set metadata attribute {attribute} on {file}")
        sys.exit(1)
    else:
        print(f"Successfully set metadata attribute {attribute} on {file} to {values}")


if __name__ == "__main__":
    main()

It doesn’t yet handle types other than string or array (need to reference here for full list of attributes/types) – kMDItemWhereFroms is an array. Finder comments and Finder tags cannot be set this way. Finder comments must be set by AppleScript and Finder tags by xattr using com.apple.metadata:_kMDItemUserTags.

I verified that both mdls and Finder show the updated kMDItemWhereFroms when set this way.

More to come – will look at adapting this for osxmetadata.