30

The Finder in Mac OS X 10.7 Lion shows a new piece of file metadata, "Date Added," which tracks the date an item was added to a folder. After upgrading to 10.7, none of the items in my ~/Downloads folder have "Date Added" values. I'd like to set all the empty "Date Added" values to match the "Date Modified" values, but I can't figure out how to set the "Date Added" attribute to a specific value.

My first guess was this:

xattr -w com.apple.metadata:kMDItemDateAdded "2012-02-19 16:34:47 +0000" myfile

But that doesn't seem to work (though it doesn't report an error either).

2
  • Did you ended up finding a solution?
    – erotsppa
    Oct 15, 2013 at 21:07
  • 1
    The accepted answer worked when I tried it (as crazy as it is). Oct 22, 2013 at 0:37

7 Answers 7

9

OK, new approach here. Caution: I don't have a system upgraded to Lion (my computer came with Lion installed) so I can't test this. Untested code; back up before trying this code!

My previous answer was based on the sort order used by the Downloads stack in the Dock. The Date Added field in the Finder appears to be based on Spotlight information, which is difficult to hack. It also isn't accessible via AppleScript. But, there does seem to be a workaround.

  1. Create a new Workflow in Automator.

  2. Set the workflow to accept files or folders from the Finder by adding the “Ask for Finder items” action.

  3. Have the workflow run an AppleScript by adding the “Run AppleScript” action.

Use this AppleScript:

on run {input, parameters}
    do shell script "sudo /usr/sbin/systemsetup -setusingnetworktime Off" with administrator privileges
    tell application "Finder"
        repeat with x in input
            set myfile to POSIX path of x
            set nm to name of x
            set d to modification date of x
            set yr to (character 3 of (year of d as string)) & (character 4 of (year of d as string))
            set mth to (month of d as number) as string
            if length of mth is 1 then set mth to "0" & mth
            set dy to day of d as string
            if length of dy is 1 then set dy to "0" & dy
            set h to hours of d as string
            if length of h is 1 then set h to "0" & h
            set m to minutes of d as string
            if length of m is 1 then set m to "0" & m
            set s to seconds of d as string
            if length of s is 1 then set s to "0" & s
            set dt to mth & ":" & dy & ":" & yr as string
            set tm to h & ":" & m & ":" & s as string
            do shell script "sudo /usr/sbin/systemsetup -setdate '" & dt & "'" with administrator privileges
            do shell script "sudo /usr/sbin/systemsetup -settime '" & tm & "'" with administrator privileges
            do shell script "mv \"" & myfile & "\" /var/tmp/clobber"
            do shell script "mv /var/tmp/clobber \"" & myfile & "\""
        end repeat
    end tell
    do shell script "sudo /usr/sbin/systemsetup -setusingnetworktime On" with administrator privileges
    return input
end run

Select the files that do not already have a Date Added (sort by Date Added in Finder, then select the part of the list without a Date Added) and run this service.

screenshot of the workflow in Automator

6
  • 2
    That is awful…in a good way, maybe? Feb 20, 2012 at 20:35
  • 1
    Well, it's definitely a hack. But it appears that particular piece of metadata is computed from Spotlight, and without hacking the dark mystery known as /.Spotlight-V100, this might be as good as it gets. But I'd love to see a clean answer.
    – Daniel
    Feb 20, 2012 at 22:14
  • I'm getting a weird error from this script? sh: -c line 0: unexpected EOF while looking for matching `"
    – erotsppa
    Oct 15, 2013 at 21:07
  • 1
    The AppleScript stopped with an error when processing a file that had $ in its name, but I worked around that by temporarily renaming that file. To make the AppleScript handle such files, my searches indicate that you could replace myfile with quoted form of myfile, but I haven’t tested that. If one tried that, it might be clearer to do the quoting when setting myfile and rename the variable to quotedFilePath. Mar 14, 2019 at 20:40
  • 1
    As of May 2019, this hack still seems to be the only way to do it and doesn't look like it will change. No scripting or CLI tool can help further because the deeper APIs to access filesystem attributes either only mention "creation" and "modification dates" (the BSD-level utime() and the Foundation level FileManager/FileAttributeKey) or mention the "added date" as a read-only value (Foundation/URL/URLResourceKey).
    – hmijail
    May 14, 2019 at 4:18
9

When I run xattr -l on items in my Downloads folder, I get a field that looks something like this:

com.apple.metadata:kMDItemDownloadedDate:
00000000  62 70 6C 69 73 74 30 30 A1 01 33 41 B4 83 4D BF  |bplist00..3A..M.|
00000010  4C 4F E3 08 0A 00 00 00 00 00 00 01 01 00 00 00  |LO..............|
00000020  00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 13                                   |.....|
00000035

This is a binary plist. When I use HexFiend to create a file with those bytes (yes, I manually entered them; blast from the past like entering assembler code out of a magazine into my Apple ][GS), then save it as a .plist file, I opened the file in TextWrangler and got the following uncompiled xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <date>2011-11-28T05:03:59Z</date>
</array>
</plist>

That said, while Apple seems to store the dates in compiled XML, plain text seems to work.

In other words, if you can get the file's modified date in string form, you can run the command xattr -w com.apple.metadata:kMDItemDownloadedDate "2012-02-19 16:34:47 +0000" file to change the "downloaded date", which appears to be the field actually sorted on, not actual Date Added.

Finally you got no error when adding the (unused) kMDItemDateAdded field because, as I learned in this article, xattr will happily set whatever metadata field you want, used or unused.

That's the core of the answer. I'll work on writing an AppleScript to get the modified date for each file, check to see if kMDItemDownloadedDate is set, and if it isn't, set kMDItemDownloadedDate to the modified date, but I wanted to get the core of the answer posted.

4
  • 1
    Run mdls on the files in your Downloads folder and you'll see the kMDItemDateAdded values. That's what the "Date Added" column in the Finder's List view shows. Feb 19, 2012 at 18:25
  • Incidentally, if you want to show the kMDItemDownloadedDate as plist information without going through HexFiend and TextWrangler, try xattr -p com.apple.metadata:kMDItemDownloadedDate FILENAME_HERE | xxd -r -p | plutil -convert xml1 - -o -. The xxd converts to binary plist data, then the plutil converts to XML plist and prints it. Feb 19, 2012 at 18:28
  • OK, I'm in over my head, I'm afraid to say. kMDItemDateAdded isn't listed in xattr -l, and kMDItemDownloadedDate isn't listed with mdls. Curiouser and curiouser. Neither is the Date Added field stored in an xattr for the directory. Where does this metadata live?
    – Daniel
    Feb 19, 2012 at 19:18
  • 1
    Since xattr is a python script, I guess it should be possible to poke around in this script and figure out how to get the binary data of the attribute in binary, rather than hex, so you can feed it directy to plutil. Feb 19, 2012 at 20:12
4

I can't find a way to set the "Date Added" shown in the Finder.

I believe you're correct that it's retrieved from the Spotlight index's kMDItemDateAdded metadata attribute. However, Spotlight appears to derive this itself in some way.

I've tried setting up an extended file attribute called com.apple.metadata:kMDItemDateAdded to a date value in one of several different formats, including the format used by kMDItemDateAdded and none of them were picked up by the Spotlight index, i.e. no matter what the value shown by xattr, the value shown by mdls wasn't changed.

I would guess, though I don't know for sure, that Spotlight simply sets this date based on the first time it indexes a file in a particular location, and doesn't check any other metadata in order to generate it. If you mv a file out of Downloads and back in, the Date Added updates to when it was moved back in, but none of the file metadata seems affected, only the Spotlight metadata.

So, in summary, I reckon Date Added is only stored somewhere in the rather cryptic guts of /.Spotlight-V100, and unless someone can come up with a way of telling Spotlight to update a metadata entry to an arbitrary value, I can't see a way of doing this.

1
  • Spotlight! Well done.
    – Daniel
    Feb 19, 2012 at 20:44
4

Thanks to Daniel Lawson for the solution! It still works well, even two years later.

I have two additions:

1) Please note that there's a small error in the code of the accepted answer.

This line:

do shell script "/usr/sbin/systemsetup -settime ''" & tm & "'"

...has an extra apostrophe, triggering an "unexpected EOF" error. It should read:

do shell script "/usr/sbin/systemsetup -settime '" & tm & "'"

2) More important, starting with Mavericks 10.9.2, systemsetup requires administrator rights. So every call to shell script should follow this formula:

do shell script "sudo /usr/sbin/systemsetup -setusingnetworktime Off" with administrator privileges

Here's the full modified version of the AppleScript, confirmed to work in 10.9.3:

on run {input, parameters}
    do shell script "sudo /usr/sbin/systemsetup -setusingnetworktime Off" with administrator privileges

    tell application "Finder"
        repeat with x in input
            set myfile to POSIX path of x
            set nm to name of x

            set d to modification date of x
            set yr to (character 3 of (year of d as string)) & (character 4 of (year of d as string))
            set mth to (month of d as number) as string
            if length of mth is 1 then set mth to "0" & mth
            set dy to day of d as string
            if length of dy is 1 then set dy to "0" & dy
            set h to hours of d as string
            if length of h is 1 then set h to "0" & h
            set m to minutes of d as string
            if length of m is 1 then set m to "0" & m
            set s to seconds of d as string
            if length of s is 1 then set s to "0" & s

            set dt to mth & ":" & dy & ":" & yr as string
            set tm to h & ":" & m & ":" & s as string
            do shell script "sudo /usr/sbin/systemsetup -setdate '" & dt & "'" with administrator privileges
            do shell script "sudo /usr/sbin/systemsetup -settime '" & tm & "'" with administrator privileges

            do shell script "mv \"" & myfile & "\" /var/tmp/clobber"
            do shell script "mv /var/tmp/clobber \"" & myfile & "\""
        end repeat
    end tell

    do shell script "sudo /usr/sbin/systemsetup -setusingnetworktime On" with administrator privileges

    return input
end run
2
  • 1
    Welcome to Ask Different! Instead of writing an answer to edit someone else's answer, simply click the edit or improve this answer button below the post that you wish to improve.
    – grg
    Jun 17, 2014 at 16:55
  • 2
    Daniel’s answer has been edited to include these changes. Jul 2, 2018 at 18:29
1

If you’re comfortable compiling some C code, then, as pointed out by Ken Thomases, this data is retrievable from getattrlist(2). And while it sounds like the corresponding settralist(2) didn’t work for the added date on older versions of macOS, the following code works for me on macOS Monterey 12.5.1, adjusting the date shown in Finder.

It’s just getting and setting the ATTR_CMN_ADDEDTIME attribute, which the manual describes as

the time that the file system object was created or renamed into its containing directory. Note that inconsistent behavior may be observed when this attribute is requested on hard-linked items

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

/*
 * Get kMDItemDateAdded of path.
 *
 * Returns:
 *   • 0 on success
 *   • 1 if a system call failed: check errno
 *   • 2 if something else went wrong
 */
int get_date_added(const char* path, struct timespec * out) {
    attrgroup_t request_attrs = ATTR_CMN_RETURNED_ATTRS | ATTR_CMN_ADDEDTIME;

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

    typedef struct {
        u_int32_t length;
        attribute_set_t returned;
        struct timespec added;
    } __attribute__((aligned(4), packed)) response_buf_t;

    response_buf_t response;

    int err = getattrlist(path, &request, &response, sizeof(response), 0);
    if (err != 0) {
        return 1;
    }
    if (response.length != sizeof(response)) {
        // Need a different-sized buffer; but provided one of exactly required
        // size?!
        return 2;
    }
    if (response.returned.commonattr != request_attrs) {
        // Didn’t get back all requested common attributes
        return 2;
    }

    out->tv_sec = response.added.tv_sec;
    out->tv_nsec = response.added.tv_nsec;

    return 0;
}

/*
 * 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;
}
1
  • Hi, could you explain how to use this please? 1. how to make this code actually run and 2. how to call it?
    – DavidT
    Oct 31, 2022 at 21:46
1

So, I've further messed with the code kindly offered by andrew and Stefan Schmidt, and here's a bit of C code that you can run after you copy your folders. Just write the path to the folder where the Date Added order is the way God intended (or just drag the folder itself into the Terminal window), press Enter, and then write the path to the folder copy, and that folder copy will receive the dates of the original! (Once again, folder with the right dates first! Or the wrong dates will overwrite the right ones!)

Code works in my test, but hey, if there's issues with it, don't be surprised, all I knew about C two days ago was it existed

EDIT: updated the code to make it more user-friendly and give more informational warnings. Remember to assert admin privileges, run it as root, sudo that sucker, lest it fail a bunch of files for no clear reason, and to for your sake only point it to folders whose dates you actually care about, lest it run for half an hour filling your logs with Application Support garbage you don't care about.

#include <stdlib.h>
#include <string.h>
#include <sys/attr.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/errno.h>
/* Code goes through all the files in the source folder and its subfolders
 * gets their Date Added
 * and sets it to the corresponding files in the destination folder
 * Neat, eh?
 */
/* List, finds all files and sub-directories of a given
 * directory in tree structure.
 * @param basePath Base path to traverse directory
 */
int ok=0;
int nok=0;
int logs=1;

void list(char *basePathSource, char *basePathDest){
  char path[9000];
  char pathd[9000];
  struct dirent *dp;
  DIR *dir = opendir(basePathSource);
  if (!dir) return;

  while ((dp = readdir(dir)) != NULL){
    if (strcmp(dp->d_name, ".") != 0 && strcmp(dp->d_name, "..") != 0){
        strcpy(path, basePathSource);   //these three lines are
        strcat(path, "/");              //equivalent to the Processing line
        strcat(path, dp->d_name);       //path=basePathSource+'/'+dp->d_name
        strcpy(pathd, basePathDest);   //these three lines are
        strcat(pathd, "/");              //equivalent to the Processing line
        strcat(pathd, dp->d_name);       //path=basePath+'/'+dp->d_name
        int terr = date_added_transfer(path, pathd);
        if(terr==0){
            ok++;
            if(logs>=2)printf("Successfully transferred Date Added to %s! :D\n", pathd);
        }else nok++;
        if (terr==2) if(logs<=2)printf("Failed to get Date Added from %s :c\n", path);
        list(path, pathd);
    }
  }
  closedir(dir);
}
/* Get kMDItemDateAdded of path.
 * Returns:
 *   • 0 on success
 *   • 1 if a system call failed: check errno
 *   • 2 if something else went wrong
 */
int get_date_added(const char* path, struct timespec * out) {
  attrgroup_t request_attrs = ATTR_CMN_RETURNED_ATTRS | ATTR_CMN_ADDEDTIME;

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

  typedef struct {
    u_int32_t length;
    attribute_set_t returned;
    struct timespec added;
  } __attribute__((aligned(4), packed)) response_buf_t;

  response_buf_t response;

  int err = getattrlist(path, &request, &response, sizeof(response), 0);
  if (err != 0) {
    if(logs<=2)printf("Failed to get Date Added from %s: '%s' :c \n", path,strerror(errno));
    return 1;
  }
  if (response.length != sizeof(response)) {
    // Need a different-sized buffer; but provided one of exactly required
    // size?!
    return 2;
  }
  if (response.returned.commonattr != request_attrs) {
    // Didn’t get back all requested common attributes
    return 2;
  }

  out->tv_sec = response.added.tv_sec;
  out->tv_nsec = response.added.tv_nsec;

  return 0;
}
/* 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) {
    if(logs<=2)printf("Failed to set Date Added to %s: '%s' :c \n", path,strerror(errno));
    return 1;
  }

  return 0;
}
/* Set kMDItemDateAdded of path to kMDItemFSContentChangeDate
 * Returns:
 *   • 0 on success
 *   • 1 if a path doesn't exist
 *   • 2 if a function call failed
 */
int date_added_transfer(char* paths, char* pathd) {
  int err;
  if(access(paths, F_OK) != 0) {
    if(logs<=2)printf("Can't reach source path %s :c\n", paths);
    return 1;
  }
  if(access(pathd, F_OK) != 0) {
    if(logs<=2)printf("Can't reach destination path %s - it's probably not there\n", pathd);
    return 1;
  }
  struct timespec out;
  err = get_date_added(paths, &out);
  if (err != 0) {
    return 2;
  }
  struct timespec in;
  in.tv_sec = out.tv_sec;
  in.tv_nsec = out.tv_nsec;
  err = set_date_added(pathd, in);
  if (err != 0) {
    return 3;
  }
  return 0;
}
/* Modify paths given as command line arguments
 * Returns:
 *   • 0 on success
 *   • 1 if a path doesn't exist
 *   • 2 if something else went wrong
 */
int main(){
  char srcpath[100];
  char dstpath[100];
  int logset;
  int loopn=1;
  // Input paths from user
  while(loopn==1){
    ok=0;
    nok=0;
    printf("Enter folder you wish to copy Date Added info from: ");
    scanf("%s", srcpath);
    printf("Enter the corresponding folder in the destination to copy the info to: ");
    scanf("%s", dstpath);
    printf("You'll be told how many successful and failed transfers occurred, In addition, type the number of the option you prefer:\n");
    printf("1. Failure messages only, 2. Failure and success messages, or 3. Success messages only\n");
    scanf("%d", &logset);
    logs=logset;
    printf("Righty then, let's go!\n");
    list(srcpath, dstpath);
    printf("%d transfers successful, %d transfers failed\n",ok,nok);
    printf("Wanna go again? 1. Yep! 2. Nah that's it!\n");
    scanf("%d", &loopn);
  }
  return 0;
}
0

Building upon the code from andrew here a program that reads the value "Date Modified" of a file and writes the value to "Date Added".

Copy the below code to a file named date_add_to_mod.c and compile it.

gcc -o date_add_to_mod date_add_to_mod.c

Call the program from the current directory with a list of files.

./date_add_to_mod file1 file2 file3

The source code of date_add_to_mod.c.

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

/*
 * For a list of files set "Date Added" in
 * macOS Finder to match "Date Modified"
 */

/*
 * Get kMDItemDateAdded of path.
 *
 * Returns:
 *   • 0 on success
 *   • 1 if a system call failed: check errno
 *   • 2 if something else went wrong
 */
int get_date_modified(const char* path, struct timespec * out) {

    attrgroup_t request_attrs = ATTR_CMN_RETURNED_ATTRS | ATTR_CMN_MODTIME;

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

    typedef struct {
        u_int32_t length;
        attribute_set_t returned;
        struct timespec modified;
    } __attribute__((aligned(4), packed)) response_buf_t;

    response_buf_t response;

    int err = getattrlist(path, &request, &response, sizeof(response), 0);
    if (err != 0) {
        return 1;
    }
    if (response.length != sizeof(response)) {
        // Need a different-sized buffer; but provided one of exactly required
        // size?!
        return 2;
    }
    if (response.returned.commonattr != request_attrs) {
        // Didn’t get back all requested common attributes
        return 2;
    }

    out->tv_sec = response.modified.tv_sec;
    out->tv_nsec = response.modified.tv_nsec;

    return 0;
}

/*
 * 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;
}

/*
 * Set kMDItemDateAdded of path to kMDItemFSContentChangeDate
 *
 * Returns:
 *   • 0 on success
 *   • 1 if a path doesn't exist
 *   • 2 if a function call failed
 */
int date_added_to_modifed(char* path) {

    int err;

    if(access(path, F_OK) != 0) {
        printf("error: %s doesn't exist\n", path);
        return 1;
    } 

    struct timespec out;
    err = get_date_modified(path, &out);
    if (err != 0) {
        return 2;
    }

    struct timespec in;
    in.tv_sec = out.tv_sec;
    in.tv_nsec = out.tv_nsec;
    err = set_date_added(path, in);
    if (err != 0) {
        return 2;
    }

    return 0;
}


/*
 * Modify paths given as command line arguments
 *
 * Returns:
 *   • 0 on success
 *   • 1 if a path doesn't exist
 *   • 2 if something else went wrong
 */
int main(int argc, char **argv) {

    if (argc >= 2) {
        for (int i = 1; i < argc; ++i) {
            char *path = argv[i];
            int err = date_added_to_modifed(path);
            if (err == 0) {
                printf("%s\n", path);
            }
            else if (err == 2) {
                printf("an unknown error occured\n");
                return 2;
            }
        }
    }
    else {
        printf("usage: date_add_to_mod file1 file2 ...\n");
    }

    return EXIT_SUCCESS;
}

The program works on macOS Catalina so probably also on macOS Big Sur (and macOS Monterey as noted above).

Further information on getattrlist and setattrlist is available in the corresponding man pages.


I have neither serious experience nor knowledge of C programming but the solution works for me™

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .