Getting the Last File Added to a Directory (Folder)


EDIT 2019/07/15 20:39 CDT:
   ● Added a working macro in addition to the bare AppleScript.


Hey Folks,

This comes up over and over, so I'm posting it independently.

The only way to reliably detect the last-added file in a folder (directory) is to use AppleScriptObjC.

(Unless you iterate through a file list and test the added-date attribute in a Keyboard Maestro action.)

This script will get the last file path.

OR – if you change the returningSingleItem flag to false it will return a list of files paths sorted by last-added.

Currently it ignores folders, but that can be changed in the code.

Get the Last File Added to a Directory (Folder) v1.00.kmmacros (12 KB)

AppleScript Code
------------------------------------------------------------------------------
# Auth: Christopher Stone { Heavy Lifting by Shane Stanley }
# dCre: 2015/09/15 20:08
# dMod: 2017/01/09 21:48
# Appl: ASObjC
# Task: Get the last-added file from a folder (directory) OR
#     : Get a list of Files sorted by Last-Added (other sort-keys listed for convenience).
# Libs: None
# Osax: None
# Tags: @Applescript, @ASObjC, @Script, @Sorted, @File, @List, @Shane, @Last, @Added, @ccs, @ccstone
# Note: Tested only on macOS 10.10.5, 10.11+, 10.12.2
------------------------------------------------------------------------------
use framework "Foundation"
use scripting additions
------------------------------------------------------------------------------

set targetFolder to path to downloads folder -- User to change as needed.
set thePath to POSIX path of targetFolder
set theSortKey to (current application's NSURLAddedToDirectoryDateKey)
set theItem to its filesIn:thePath sortedBy:theSortKey returningSingleItem:true

------------------------------------------------------------------------------
--» HANDLERS
------------------------------------------------------------------------------

on filesIn:folderPOSIXPath sortedBy:sortKey returningSingleItem:singleItemBoolVal
   
   # Specify keys for the values we want for each item in the folder.
   # We want the path, whether it's a directory, whether it's a package, and the key to sort on.
   set keysToRequest to {current application's NSURLPathKey, ¬
      current application's NSURLIsPackageKey, ¬
      current application's NSURLIsDirectoryKey, ¬
      sortKey}
   
   # Make a NSURL for the folder because that's what's needed.
   set theFolderURL to current application's class "NSURL"'s fileURLWithPath:folderPOSIXPath
   
   # Get a reference to the file manager.
   set theNSFileManager to current application's NSFileManager's defaultManager()
   
   # Get the NSURLs of items in folder while at the same time getting values for our keys and skipping invisible items.
   set listOfNSURLs to (theNSFileManager's contentsOfDirectoryAtURL:theFolderURL ¬
      includingPropertiesForKeys:keysToRequest ¬
      options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) ¬
      |error|:(missing value))
   
   # Create an array in which to store just the key values.
   set valuesNSArray to current application's NSMutableArray's array()
   
   # Loop through the NSURL list retrieving key values and adding each set as a dictionary/record to the array/list.
   repeat with oneNSURL in listOfNSURLs
      (valuesNSArray's addObject:(oneNSURL's resourceValuesForKeys:keysToRequest |error|:(missing value)))
   end repeat
   
   # Filter out all non-packages directories using a predicate.
   set theNSPredicate to current application's NSPredicate's predicateWithFormat_("%K == NO OR %K == YES", current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey)
   set valuesNSArray to valuesNSArray's filteredArrayUsingPredicate:theNSPredicate
   
   # Make a sort descriptor that describes the specific sort key.
   set theDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:(sortKey) ascending:true
   
   # Sort the array.
   set theSortedNSArray to valuesNSArray's sortedArrayUsingDescriptors:{theDescriptor}
   
   if singleItemBoolVal = false then
      # Extract just the paths and convert to an AppleScript list.
      return (theSortedNSArray's valueForKey:(current application's NSURLPathKey)) as list
   else
      # If you want the single most-recent file, use this instead of the previous line.
      return (theSortedNSArray's lastObject()'s valueForKey:(current application's NSURLPathKey)) as text
   end if
   
end filesIn:sortedBy:returningSingleItem:

------------------------------------------------------------------------------

(*

OTHER SORT CRITERIA

NSURL AddedToDirectoryDateKey
NSURL AttributeModificationDateKey
NSURL ContentAccessDateKey
NSURL ContentModificationDateKey
NSURL CreationDateKey
NSURL FileAllocatedSizeKey
NSURL FileSizeKey
NSURL LabelColorKey
NSURL LabelNumberKey
NSURL LocalizedLabelKey
NSURL LocalizedTypeDescriptionKey
NSURL NameKey
NSURL TotalFileAllocatedSizeKey
NSURL TotalFileSizeKey
NSURL TypeIdentifierKey

* REMOVE THE SPACE AFTER 'NSURL' TO USE!

*)

------------------------------------------------------------------------------

-Chris

9 Likes

Very handy! Thanks.

Awesome!

I don’t need this now, but I’ve needed it plenty of times before, and I know I will again, so this has gone directly in my list of resources, leaving me with a warm and fuzzy feeling. :slight_smile: It’s so nice to know I have a useful tool in my toolbox, just waiting for that time when it’s needed, so I won’t have to search the hardware store for it!

Thanks for putting this in its own script!

In a JavaScript for Automation version (‘JXA’ in the minds of a few, though not in Apple’s documents :slight_smile: ) you can quite easily use JS itself to do the sorting.

(The NSURL keys are built-in properties of the $ object (ObjC.$)).

Example below, retrieving the AddedToDirectory and ContentModification dates, together with the TypeIdentifier. The sorting on AddedToDirectory is done in JS.

(function () {
    'use strict';

    var strPath = '~/Desktop'; // Choose the folder you want

    // lstKeys :: [$.Keys]   // Choose the keys you want
    var lstKeys = [
        $.NSURLAddedToDirectoryDateKey,
        $.NSURLContentModificationDateKey,
        $.NSURLTypeIdentifierKey
    ].map(ObjC.unwrap);

    // folderFilesWithKeyValues :: String -> [$.NSURLConstant] -> [Dictionary]
    var folderFilesWithKeyValues = function (strPath, lstKeys) {
        var fm = $.NSFileManager.defaultManager,
            oURL = $.NSURL.fileURLWithPathIsDirectory($(strPath)
                .stringByStandardizingPath, true);

        return ObjC.unwrap(
                fm.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError(
                    oURL, lstKeys, 1 << 2, null
                )
            )
            .map(function (x) {
                var dctProps = {
                    path: x.path.js
                };

                lstKeys.forEach(function (k) {
                    var ref = $();
                    x.getResourceValueForKeyError(ref, k, null);
                    dctProps[k.slice(5, -3)] = ref.js;
                });

                return dctProps;
            });
    };

    // GENERIC FUNCTIONS -----------------------------------------------------

    // show :: a -> String
    var show = function (x) {
        return JSON.stringify(x, null, 2);
    };

    // sortWith :: Ord b => (a -> b) -> [a] -> [a]
    var sortWith = function (f, xs) {
        return sortBy(compare(f), xs);
    };

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    var sortBy = function (f, xs) {
        return xs.sort(f);
    };

    // compare :: (a -> b) -> (a -> a -> Ordering)
    var compare = function (f) {
        return function(x, y) {
            var a = f(x),
                b = f(y);
            return a < b ? -1 : a > b ? 1 : 0
        };
    };
    
    

    // TEST ------------------------------------------------------------------
    return show(sortWith(function (x) {
            return x.AddedToDirectoryDate;
        }, folderFilesWithKeyValues(strPath, lstKeys))
        .map(function (x) {
            return [  // Return the path, and the attributes by key
                     //  (dropping 'NSURL' from start and 'key' from end)
                x.path,
                x.AddedToDirectoryDate,
                x.ContentModificationDate,
                x.TypeIdentifier
            ];
        }));
})();


Sample output:

[
  [
    "/Users/houthakker/Desktop/TP fixes",
    "2017-01-03T01:30:51.000Z",
    "2017-01-03T01:33:21.000Z",
    "public.folder"
  ],
  [
    "/Users/houthakker/Desktop/TuringTestTerritory2.pdf",
    "2017-01-04T22:46:31.000Z",
    "2017-01-04T22:46:31.000Z",
    "com.adobe.pdf"
  ],
  [
    "/Users/houthakker/Desktop/evenOdd.scpt",
    "2017-01-12T18:09:36.000Z",
    "2017-01-13T10:34:23.000Z",
    "com.apple.applescript.script"
  ],
  [
    "/Users/houthakker/Desktop/formatTest.zip",
    "2017-01-13T10:32:08.000Z",
    "2017-01-13T10:32:08.000Z",
    "public.zip-archive"
  ],
  [
    "/Users/houthakker/Desktop/formatTest.zip.cpgz",
    "2017-01-13T10:32:33.000Z",
    "2017-01-13T10:32:33.000Z",
    "com.apple.bom-compressed-cpio"
  ],
  [
    "/Users/houthakker/Desktop/compiled.scpt",
    "2017-01-13T10:46:09.000Z",
    "2017-01-13T10:46:09.000Z",
    "com.apple.applescript.script"
  ],
  [
    "/Users/houthakker/Desktop/sample.graffle",
    "2017-01-13T15:23:55.000Z",
    "2017-01-13T15:23:55.000Z",
    "com.omnigroup.omnigraffle.graffle"
  ]
]
3 Likes

10 posts were split to a new topic: Musings on “JXA” vs “JavaScript for Automation”

EDIT 2019/07/15 20:39 CDT to Post #1
    ● Added a working macro in addition to the bare AppleScript.

-ccs

1 Like

Or, FWIW, updating the code in

https://forum.keyboardmaestro.com/t/getting-the-last-file-added-to-a-directory-folder/6002/4

above to (Sierra onwards) ES6 JavaScript:

(() => {
    'use strict';

    // Ver 0.2 (ES6)

    const strPath = '~/Desktop'; // Choose the folder you want

    // lstKeys :: [$.Keys]   // Choose the keys you want
    const lstKeys = [
        $.NSURLAddedToDirectoryDateKey,
        $.NSURLContentModificationDateKey,
        $.NSURLTypeIdentifierKey
    ].map(ObjC.unwrap);

    // folderFilesWithKeyValues :: String -> [$.NSURLConstant] -> [Dictionary]
    const folderFilesWithKeyValues = (strPath, lstKeys) => {
        const
            fm = $.NSFileManager.defaultManager,
            oURL = $.NSURL.fileURLWithPathIsDirectory($(strPath)
                .stringByStandardizingPath, true);

        return ObjC.unwrap(
                fm.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError(
                    oURL, lstKeys, 1 << 2, null
                )
            )
            .map(fp => lstKeys.reduce((dct, k) => {
                const ref = $();
                return (
                    fp.getResourceValueForKeyError(ref, k, null),
                    Object.assign(dct, {
                        [k.slice(5, -3)]: ref.js
                    })
                );
            }, {
                path: fp.path.js
            }))
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // show :: a -> String
    const show = x =>
        JSON.stringify(x, null, 2);

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = (f, xs) =>
        xs.sort(f);

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        (x, y) => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // TEST -----------------------------------------------
    return show(sortBy(
            comparing(x => x.AddedToDirectoryDate),
            folderFilesWithKeyValues(strPath, lstKeys))
        .map(x => [ // Return the path, and the attributes by key
            //  (dropping 'NSURL' from start and 'key' from end)
            x.path,
            x.AddedToDirectoryDate,
            x.ContentModificationDate,
            x.TypeIdentifier
        ]));
})();
2 Likes

Thanks Chris for the script.

How can I modify it to get only the filename without the full path?

If Chris' script returns the file path to the Execute AppleScript Action, which I would expect, then you can use that variable to get the file name using this:

Get File Attribute action

Thank you so much ccstone! I've been trying to do this reliably for years. The code you post in this forum are like secret weapons only KM users get to try sometimes.

1 Like