Search code examples
cswiftmacosdarwin

Change kMDItemDateAdded ( "Date added" ) file's metadata attribute via Swift code


I'm need to change kMDItemDateAdded ( "Date added" ) file's metadata attribute.

There are absent such ability via

var attrs: [FileAttributeKey: Date] = [:]
//no ability to change FileAttributeKey.dateAdded - it is absent
attrs[FileAttributeKey.modificationDate] = Date.now 

try FileManager.default.setAttributes(attrs, ofItemAtPath: file.path)

All I have found is C language code:

#include <stdlib.h>
#include <string.h>
#include <sys/attr.h>
#include <unistd.h>

/*
 * Set kMDItemDateAdded of path.
 *
 * Returns:
 *   • 0 on success
 *   • 1 if a system call failed: check errno
 */
int set_date_added(const char* path, struct timespec in) {
    attrgroup_t request_attrs = ATTR_CMN_ADDEDTIME;

    struct attrlist request;
    memset(&request, 0, sizeof(request));
    request.bitmapcount = ATTR_BIT_MAP_COUNT;
    request.commonattr = request_attrs;

    typedef struct {
        struct timespec added;
    } __attribute__((aligned(4), packed)) request_buf_t;

    request_buf_t request_buf;
    request_buf.added.tv_sec = in.tv_sec;
    request_buf.added.tv_nsec = in.tv_nsec;

    int err = setattrlist(path, &request, &request_buf, sizeof(request_buf), 0);
    if (err != 0) {
        return 1;
    }

    return 0;
}

Code taken from: https://gist.github.com/tbussmann/b6473db60dc622233ec8720f65c9b450

I need help to transfer "set_date_added" func to swift code.

All I have at the moment is:

import Darwin

func setDateAdded(path: String, toDate date: Date) -> Bool {
    guard FileManager.default.fileExists(atPath: path) else { return false }
    
    var attrLst = attrlist()
    attrLst.bitmapcount = u_short(ATTR_BIT_MAP_COUNT)
    attrLst.commonattr  = attrgroup_t(ATTR_CMN_ADDEDTIME)
    
    struct attr_buf_t {
        var added: timespec
    }
    
    var attr_buf = attr_buf_t(added: timespec(tv_sec: Int(date.timeIntervalSince1970), tv_nsec: 0) )
    
    let memSize = MemoryLayout.size(ofValue: attr_buf)
    
    let err = setattrlist(path, &attrLst, &attr_buf, memSize, 0)
    
    return err == 0
}

( setattrlist() returns -1 )

Maybe this will be helpful: documentation for setattrlist


Solution

  • Interestingly, your Swift code worked perfectly for me. It successfully updated the kMDItemDateAdded attribute and did not throw any errors.

    If you scroll down the setattrlist documentation that you linked, you can see that there are several possible errors. You can see exactly which one is returned by importing System in your Swift file, then on the line after you call setattrlist add:

    if err != 0, let msg = strerror(errno) {
        print(String(cString: msg))
    }
    

    Compare the error message with the errors listed in the documentation, and you'll quickly figure out what's going wrong.

    Since the code works for me, it might be an issue with your permissions or the path name. I tested this from a sandboxed SwiftUI app, and it works, provided you import the file's URL with a .fileImporter modifier, and then perform the operation between calls to startAccessingSecurityScopedResource and startAccessingSecurityScopedResource. Whether you are using SwiftUI or AppKit, you need to first get the URL from the relevant document picker interface. Directly instantiating a URL to the path and then calling the ...SecurintScopedResource methods doesn't work. And if you need longer access, you can use URL bookmarks, which I've never done before, but you can read about here: https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox?language=objc .

    It is also worth noting that according to the setattlist docs, only the file's owner can change the Date Added attribute:

    You must own the file system object in order to set any of the following attributes:
    
    ATTR_CMN_GRPID
    ATTR_CMN_ACCESSMASK
    ATTR_CMN_FLAGS
    ATTR_CMN_CRTIME
    ATTR_CMN_MODTIME
    ATTR_CMN_ACCTIME
    ATTR_CMN_ADDEDTIME
    

    The only way to change those attributes for a file you do not own is to change the file's owner to the current user, which requires root privileges.

    Permissions aside, there are some potential issues with your code that could cause problems when working with the getattrlist and setattrlist functions in Swift in general.

    First, Swift structs aren't guaranteed to have the same memory layout as C structs, so passing in the pointer to your struct might not work with a struct with more properties. Also, if you dig into the getattrlist documentation, you'll see that the C structs have to be properly packed (which is why the C code you translated used a packed struct.) Here's what the getattrlist docs have to say:

    1. The first element of the buffer is a u_int32_t that contains the overall length, in bytes, of the attributes returned. This size
      includes the length field itself.

    2. Following the length field is a list of attributes. Each attribute is represented by a field of its type, where the type is given as part of the attribute description (below).

    3. The attributes are placed into the attribute buffer in the order that they are described below.

    4. Each attribute is aligned to a 4-byte boundary (including 64-bit data types).

    (See: https://www.unix.com/man-page/mojave/2/getattrlist/)

    Second, in the setattrlist documentation, the example shows the buffer first being populated by a call to getattrlist, which allows you to update existing properties.

    There are two ways to handle this safely. First, if possible, simply define the struct for your buffer in a C header, and add it to your projects bridging header (or create a C target in an SPM project and import it).

    However, if you want to use pure Swift, you might not be able to use a struct, but there is no reason the buffer has to be a struct. You can add up the the size of all of the fields, allocate an UnsafeMutableRawPointer of the appropriate size and alignment, and then populate the buffer. I made a wrapper class that uses Swift's new parameter packs to try this out. You can take a look in this gist: https://gist.github.com/elopinto/95c46981098003aa58cb9a8344c66ab8

    And using that, we can update your function with improved error handling and slightly more flexibility with the buffer:

    func setDateAdded(path: String, toDate date: Date) -> Bool {
        guard FileManager.default.fileExists(atPath: path) else { return false }
        
        var attrLst = attrlist()
        attrLst.bitmapcount = u_short(ATTR_BIT_MAP_COUNT)
        attrLst.commonattr  = attrgroup_t(ATTR_CMN_ADDEDTIME)
        
        //Always start with UInt32 for the length field.
        let attrBuff = AttrBuffer(attrTypes: UInt32.self, timespec.self)
        var err = getattrlist(path, &attrLst, attrBuff.buffer, attrBuff.size, 0)
        
        if err != 0, let msg = strerror(errno) {
            print(String(cString: msg))
            return false
        }
        
        let tsPtr = attrBuff.attribs.1
        tsPtr.pointee = timespec(tv_sec: Int(date.timeIntervalSince1970), tv_nsec: 0)
        
        // Alternatively, we can update the existing value, by, e.g., one minute
        // tsPtr.pointee.tv_sec += 60
        
        // We use the pointer to the field to update, not the whole buffer here.
        err = setattrlist(path, &attrLst, tsPtr, MemoryLayout.size(ofValue: tsPtr.pointee), 0)
        
        if err != 0, let msg = strerror(errno) {
            print(String(cString: msg))
            return false
        }
        
        return true
    }