Forum Discussion

vladenache's avatar
vladenache
Regular Visitor
1 day ago

sftp file modified date

Hei 

Having a hard time picking up the modified datetime of files from a sftp server.
With a Get MEtadata activity, the Last Modified field actually is just the time when I run that activity - so that doesn't help me at all.

 

I need to keep track of that timestamp, so I can filter and pickup only new files from the SFTP server

Anybody can provide some suggestion? Really grateful!

 

 

 

8 Replies

  • Prince0011's avatar
    Prince0011
    Icon for Memorable Member rankMemorable Member

    Hi,

    Two things to check. If you run Get Metadata on the folder, you only get the folder’s timestamp, and some SFTP servers report that as the current time. Instead, get the childItems, loop over them with a ForEach, and run Get Metadata on each file to read its own Last Modified.

    The easier route is to skip it and use the Copy activity’s “Filter by last modified” (start and end time) on the SFTP source. Store the last run time in a watermark table or pipeline variable and pass it in, so only new files get picked up.

    If this helped, feel free to give it a kudos or mark it as solution, helps others find it too.

  • Hi vladenache​, you should be able to solve with the following steps in the pipeline,

    1. Add Get Metadata activity on the folder and select field list Child items.
    2. Add a ForEach using @activity('Get Metadata1').output.childItems.
    3. Inside the foreach loop, add a second Get Metadata activity and use @item().name in the file path. In the field list select Item name and Last modified.

    ,

     

  • ShivekMaharaj's avatar
    ShivekMaharaj
    Icon for Community Champion rankCommunity Champion

    Hi vladenache​,

    The approach above will let you retrieve the actual file-level Last modified value. I would add one more step if your end goal is incremental pickup rather than only displaying that timestamp.

    A practical pattern is:

    • Get the folder child items.
    • ForEach file, retrieve its lastModified metadata.
    • Compare that value against a persisted watermark representing the last successfully processed timestamp.
    • Process only files newer than that watermark.
    • Update the watermark only after the pipeline completes successfully.


    Alternatively, if the requirement is simply to copy files modified after a known timestamp, the Fabric SFTP Copy activity already supports Filter by last modified. You can provide a UTC Start time so that only files whose modified timestamp is greater than or equal to that value are selected.

    I would avoid using utcNow() as the watermark at the start of every run, because a failed or long-running pipeline could then create gaps. Persisting the maximum successfully processed lastModified value is generally safer.

    Also keep in mind that childItems itself only returns the child item name/type, so you still need the second Get Metadata activity if you specifically want each individual file's lastModified value.

    AI-assisted drafting: AI was used to help structure and phrase this response. I reviewed and validated the technical content before posting.

  • jubinsoni's avatar
    jubinsoni
    Icon for Memorable Member rankMemorable Member

    Hi vladenache​,

    Echoing nielsvdc​ and ShivekMaharaj​: the folder-level metadata call returns only the child items, so the real last modified value comes from a second Get Metadata call per file inside a ForEach. Since your end goal is picking up only new files, here is the complete incremental pattern around that, with the choices you will need to make.

    The pattern, step by step

    StepActivityWhat it does
    1Lookup on a watermark tableReads the last processed modified timestamp for this source
    2Get Metadata on the folder, field list Child itemsReturns the list of file names
    3ForEach over the child itemsRuns once per file
    4Get Metadata inside the loop, using the file name, field list Item name and Last modifiedGets the real last modified value for that file
    5If Condition comparing last modified with the watermarkOnly continues for files newer than the watermark
    6Copy activity for the new fileLoads it into the Lakehouse or Warehouse
    7After the loop, update the watermarkStores the largest last modified value that was processed successfully

    The condition inside the loop

    @and(
        greater(activity('Get File Metadata').output.lastModified, variables('LastWatermark')),
        endswith(item().name, '.csv')
    )

    The activity name and the variable name are placeholders for whatever you call them in your pipeline.

    Choices that matter

    TopicRecommendation
    Watermark update timingUpdate it only after the loop finished without failure, otherwise a failed run can skip files that were never loaded
    Files written while you readFiles still being uploaded can have a recent modified time; use an age buffer (only take files older than a few minutes) or a done marker file
    Same timestamp on several filesUse greater than or equal and remember which files were processed, or keep a log table of loaded file names
    Time zoneCompare timestamps in one time zone; the server and the pipeline may differ
    Large foldersA Get Metadata call per file is fine for hundreds of files; for many thousands, consider a copy activity setting that filters by last modified time directly on the source, if your connector supports it, and compare the options in the documentation
    Re-processingKeep a log table of file name, modified time and load time, so a file can be reloaded on purpose

    With a log table you also get an audit trail for free, which is helpful when someone asks why a file did not appear.

    So my overall approach would be:

    1. Keep a watermark (and ideally a file log) in a control table
    2. List files, then get each file's real last modified inside a ForEach
    3. Copy only files newer than the watermark, with a small age buffer
    4. Update the watermark after a fully successful run
    5. Compare time zones and handle equal timestamps

    AI-assisted drafting: AI was used to help structure and phrase this response. I reviewed and validated the technical content before posting.

  • vladenache's avatar
    vladenache
    Regular Visitor

    hei everyone, I appreciate all your answers, but this doesn't seem to work for me.
    when I run a Get Metadata activity with a specific file name, and retreive the last modified datetime, it returns the actual time when the activity was run. like in this example. the example file has a hardcoded timestamp in the name to show when it was produced, but look at the last modified value i get, it is exactly UTC time when i ran the activity:


    So I know for a fact the file was uploaded on the 10th of november, not the minute I ran the activity.

    As a clarification, not all files have a hardcoded datetime in their name, so I cannot rely on extracting the actual value from the item name alone.

     

    Thanks

  • vladenache​  One detail that can save you the per-file Get Metadata calls: Get Metadata can filter by last modified itself. Set modifiedDatetimeStart and modifiedDatetimeEnd in its store settings, and childItems then returns only the files modified in that range. The filter works only when Get Metadata points at a folder, not a file. Times are UTC, and files in subfolders aren't included.
    1. Point Get Metadata at the folder with field list Child items, and set the window. In the activity JSON it sits here:

    "storeSettings": {
        "type": "SftpReadSettings",
        "modifiedDatetimeStart": "@{variables('LastWatermark')}",
        "modifiedDatetimeEnd": "@{pipeline().TriggerTime}"
    }

    2. Feed childItems into the ForEach and Copy, so only new files reach the loop.
    3. After a fully successful run, store the same TriggerTime as the new watermark. The window is start inclusive and end exclusive, so consecutive runs neither skip nor repeat a file.
    4. If you filter in the Copy activity instead, note that the last modified filter is ignored when File path type is List of files.
    If a file-level Get Metadata still returns the run time, compare the same file's timestamp in an SFTP client such as WinSCP. Both Get Metadata and the Copy filter read the Last Modified attribute the server reports. If the server doesn't report a real one, neither filter will work, and a log table of processed file names (or a date in the file name) is the fallback.
    Note: this filter is documented on the Azure Data Factory Get Metadata page. The Fabric Get Metadata page doesn't mention it, so test it once on a small folder.
    If this solves it, please mark it as the solution.

     

    References:
    https://learn.microsoft.com/en-us/azure/data-factory/control-flow-get-metadata-activityhttps://learn.microsoft.com/en-us/fabric/data-factory/connector-sftp-copy-activity 

  • vladenache's avatar
    vladenache
    Regular Visitor

    hei ShafiUllah-bc​ , thanks for the message.

    if you understand correctly, you propose that the trigger time should be saved as a watermark - however, my idea is that i should rely on the actual modified time of the file from the sftp server, not the time i trigger a run. thus, i still need a succesfull way of accessing that particular file atribute through fabric

    • ShafiUllah-bc's avatar
      ShafiUllah-bc
      Icon for Resolver II rankResolver II

      vladenache​ You're right. The watermark should be the file's own lastModified value from the SFTP server, not the time the pipeline is triggered. That last step in my previous reply was wrong on that point.
      The Fabric SFTP connector supports Get Metadata, and lastModified is a valid field for a file. childItems on a folder only returns name and type, so to read the attribute you still point a second Get Metadata at each file.
      1. Lookup the stored watermark, which is the max lastModified from the last successful run.
      2. Get Metadata on the folder with field list Child items.
      3. ForEach over @activity('Get Metadata1').output.childItems. Inside the loop, Get Metadata on @item().name with field list Last modified.
      4. Copy the file only when that value is greater than the watermark, and keep the max lastModified you see.
      5. After a fully successful run, write that max lastModified back as the new watermark.

      @greater(activity('Get File Metadata').output.lastModified, variables('LastWatermark'))

      If you only need to copy the new files, set Filter by last modified on the Copy source instead. Put the stored watermark in Start time (UTC) and leave End time empty. That filter uses the file's lastModified from the server, not the trigger time. Start is inclusive and end is exclusive, times are UTC in the form yyyy-mm-ddThh:mm:ss.fffZ, and the filter is ignored when File path type is List of files. Because start is inclusive, files whose lastModified equals the watermark will be selected again, which is why the condition above uses greater(). After a successful copy, still update the watermark to the max lastModified you read, not to the run time. If you use Get Metadata's own last modified filter, it enumerates every file in the folder, so avoid pointing it at a large directory. Files in subfolders are not included.
      To check it, run the file-level Get Metadata on one file and compare output.lastModified with the same file's timestamp in an SFTP client. They should match, and it should not equal the activity run time.
      If Get Metadata still returns the run time, the Copy filter will too, because both read the Last Modified attribute the server reports. If the notebook can reach the SFTP host, paramiko can read that attribute directly. Install paramiko if it isn't already available, and keep the password in Key Vault:

      import paramiko
      from datetime import datetime, timezone
      
      password = notebookutils.credentials.getSecret('https://<name>.vault.azure.net/', 'sftp-password')
      ssh = paramiko.SSHClient()
      ssh.load_system_host_keys()
      ssh.connect(hostname='<host>', username='<user>', password=password)
      sftp = ssh.open_sftp()
      print(datetime.fromtimestamp(sftp.stat('/path/to/file').st_mtime, timezone.utc))
      for attr in sftp.listdir_attr('/path/to/folder'):
          print(attr.filename, datetime.fromtimestamp(attr.st_mtime, timezone.utc))
      sftp.close()
      ssh.close()

      If paramiko or the client shows the real time and Get Metadata shows the run time, the connector is not returning the server attribute. If all of them show the run time, the server is not reporting a usable Last Modified value, and a log of processed file names is the fallback.

      References:
      https://learn.microsoft.com/en-us/fabric/data-factory/connector-sftp-overview
      https://learn.microsoft.com/en-us/azure/data-factory/control-flow-get-metadata-activity
      https://learn.microsoft.com/en-us/fabric/data-factory/connector-sftp-copy-activity