solution for one base directory for versioning?

Get help for specific problems
Posts: 2
Joined: 9 Aug 2026

der_reisende

Hello everyone,
I read several posts here about this topic, but I think I did not find a suitable solution.
I have several folder pairs on different drives that I want to synchronize.
My goal is to store all old versions in a single directory
[ioddmsc]\Revisions\<date>, but from here in different subdirectories.
The current solution, to have one base directory \Revisions will copy all files into this one directory \Revisions\2026-08-10, which will make a restore next to impossible and might overwrite files of the same name.

So, for the examples below, the revisions shall be stored in from D:\MSC\Documents to [ioddmsc]\Revisions\2026-08-10\Backup\MSC\Documents (or ...<date>\MSC\Documents), from C:\eclipse-javascript\oxygen to [ioddmsc]\Revisions\2026-08-10\Backup\c-eclipse-javascript-oxygen or at least to ...<date>\eclipse-javascript-oxygen\).

The only solutions I found up till now is to have one Revisions Directory for each FolderPair, but then these Revisions would not be stored in one base directory \Revisions\2026-08-10, but I would have several directories 2026-08-10 spread among different folders.

Is there a way to achieve this?

Thanks

<Synchronize>
<Changes>
<Left Create="right" Update="right" Delete="right"/>
<Right Create="right" Update="right" Delete="right"/>
</Changes>
<DeletionPolicy>Versioning</DeletionPolicy>
<VersioningFolder Style="TimeStamp-Folder" MaxCount="5">[ioddmsc]\Revisions</VersioningFolder>
</Synchronize>


<FolderPairs>
<Pair>
<Left>d:\MSC\Documents</Left>
<Right>[ioddmsc]\Backup\MSC\Documents</Right>
</Pair>
<Pair>
<Left>d:\svn</Left>
<Right>[ioddmsc]\Backup\svn</Right>
</Pair>
<Pair>
<Left>d:\tools</Left>
<Right>[ioddmsc]\Backup\d-tools</Right>
</Pair>
<Pair>
<Left>D:\stick</Left>
<Right>[ioddmsc]\Backup\stick</Right>
</Pair>
<Pair>
<Left>C:\eclipse-javascript-oxygen</Left>
<Right>[ioddmsc]\Backup\c-eclipse-javascript-oxygen</Right>
</Pair>
<Pair>
<Left>D:\eclipse</Left>
<Right>[ioddmsc]\Backup\d-eclipse</Right>
</Pair>
<Pair>
<Left>c:\totalcmd</Left>
<Right>[ioddmsc]\Backup\c-totalcmd</Right>
</Pair>
</FolderPairs>
User avatar
Posts: 2995
Joined: 22 Aug 2012

Plerry

As described as option 3 in the Versioning Manual page, you can specify your Versioning location(s) using a Macro, as something like
[Versioning Location]/%Date%
and then use the Naming Convention "Replace" (or "Time stamp [File]")
If you want or need to keep track of the left-right folder pair your Versions originate from, you can use a unique imaginary pair name or ID [PairNameOrID] for each left-right pair and specify a local Versioning location per left-right pair like
[Versioning Location]/%Date%/[PairNameOrID] for that pair.

Note that only retaining the (in your setup) youngest 5 previous versions almost certainly will not work in above setup.
You will need to delete older versions manually, e.g. based on date.
Posts: 2
Joined: 9 Aug 2026

der_reisende

thank you very much, this works great!

in case someone is interested, I had ChatGPT write me a script which works as a replacement for the MaxCount functionality. Note that it distinguishes between "old" Revisions and "new" Revisions.
"old" ones are marked with a timestamped directory, "new" ones with just a date directory.
In my old Revisions, the base directory had not been preserved, so comparing between old and new Revisions is a bit fuzzy.

It can be called as a follow up command upon success like this
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "d:\stick\backup-ffs\Cleanup-Backups.ps1"
# ============================================================
# Cleanup-Backups.ps1
#
# Usage:
#
#   Dry run (default):
#       powershell.exe -File .\Cleanup-Backups.ps1
#
#   Actually delete:
#       powershell.exe -File .\Cleanup-Backups.ps1 -Delete
#
#
# Directory formats:
#
#   YYYY-MM-DD
#       G:\Revisions\YYYY-MM-DD\<basedir>\<subfolder>\<file>
#
#   YYYY-MM-DD HHMMSS
#       G:\Revisions\YYYY-MM-DD HHMMSS\<subfolder>\<file>
#
#
# Duplicate rules:
#
#   Date-only backups:
#       key = <basedir>\<subfolder>\<file>
#
#   Timestamped backups:
#       key = <subfolder>\<file>
#
# Since old timestamped backups are missing <basedir>, a
# timestamped file can match the END of a date-only path.
#
# A timestamped file is assigned to AT MOST ONE date-only
# group. If multiple groups could match it, the alphabetically
# first matching group gets it.
#
# Timestamped files that don't match any date-only group are
# grouped by their exact relative path.
# ============================================================


param(
    [switch]$Delete
)

$DryRun = -not $Delete

# ------------------------------------------------------------
# Configuration
# ------------------------------------------------------------

$BackupRoot = "G:\Revisions"
$KeepCopies = 5


# ------------------------------------------------------------
# Find revision directories
# ------------------------------------------------------------

$RevisionDirs = Get-ChildItem -LiteralPath $BackupRoot -Directory |
    Where-Object {
        $_.Name -match '^\d{4}-\d{2}-\d{2}( \d{6})?$'
    } |
    ForEach-Object {

        $Date = $null
        $DateOnly = $false

        # YYYY-MM-DD HHMMSS
        if ($_.Name -match '^\d{4}-\d{2}-\d{2} \d{6}$') {

            $Date = [datetime]::ParseExact(
                $_.Name,
                'yyyy-MM-dd HHmmss',
                [Globalization.CultureInfo]::InvariantCulture
            )
        }

        # YYYY-MM-DD
        elseif ($_.Name -match '^\d{4}-\d{2}-\d{2}$') {

            $Date = [datetime]::ParseExact(
                $_.Name,
                'yyyy-MM-dd',
                [Globalization.CultureInfo]::InvariantCulture
            )

            $DateOnly = $true
        }

        if ($null -ne $Date) {

            [PSCustomObject]@{
                Directory = $_
                Date      = $Date
                DateOnly  = $DateOnly
            }
        }
    }


# ------------------------------------------------------------
# Find all files
# ------------------------------------------------------------

$AllFiles = foreach ($Revision in $RevisionDirs) {

    if ($Revision.DateOnly) {

        # ----------------------------------------------------
        # YYYY-MM-DD
        #
        # The first directory below the revision directory is
        # <basedir>.
        # ----------------------------------------------------

        $BaseDirectories = Get-ChildItem `
            -LiteralPath $Revision.Directory.FullName `
            -Directory


        foreach ($BaseDirectory in $BaseDirectories) {

            Get-ChildItem `
                -LiteralPath $BaseDirectory.FullName `
                -File `
                -Recurse |
                ForEach-Object {

                    # Result:
                    #
                    #   <basedir>\<subfolder>\<file>
                    #
                    $RelativePath = $_.FullName.Substring(
                        $Revision.Directory.FullName.Length + 1
                    )

                    [PSCustomObject]@{
                        File         = $_
                        RelativePath = $RelativePath
                        DateOnly     = $true
                        RevisionDate = $Revision.Date
                        RevisionDir  = $Revision.Directory.FullName
                    }
                }
        }
    }
    else {

        # ----------------------------------------------------
        # YYYY-MM-DD HHMMSS
        #
        # Old format: <basedir> is missing.
        # ----------------------------------------------------

        Get-ChildItem `
            -LiteralPath $Revision.Directory.FullName `
            -File `
            -Recurse |
            ForEach-Object {

                # Result:
                #
                #   <subfolder>\<file>
                #
                $RelativePath = $_.FullName.Substring(
                    $Revision.Directory.FullName.Length + 1
                )

                [PSCustomObject]@{
                    File         = $_
                    RelativePath = $RelativePath
                    DateOnly     = $false
                    RevisionDate = $Revision.Date
                    RevisionDir  = $Revision.Directory.FullName
                }
            }
    }
}


# ------------------------------------------------------------
# Separate the two types of backups
# ------------------------------------------------------------

$DateOnlyFiles = @(
    $AllFiles | Where-Object { $_.DateOnly }
)

$TimestampedFiles = @(
    $AllFiles | Where-Object { -not $_.DateOnly }
)


# ------------------------------------------------------------
# Group date-only files.
#
# Their complete path, including basedir, is significant.
# ------------------------------------------------------------

$DateOnlyGroups = @(
    $DateOnlyFiles |
        Group-Object RelativePath |
        Sort-Object Name
)


# ------------------------------------------------------------
# Assign every timestamped file to AT MOST ONE date-only
# group.
#
# Example:
#
#   timestamped:
#       readme.md
#
# could match:
#
#   project1\readme.md
#   project2\readme.md
#
# It is assigned to only one of them.
#
# We use alphabetical order to make the decision deterministic.
# ------------------------------------------------------------

$AssignedTimestamped = @{}

foreach ($Timestamped in $TimestampedFiles) {

    $MatchingGroups = @(
        $DateOnlyGroups |
            Where-Object {

                $GroupPath = $_.Name
                $TimestampedPath = $Timestamped.RelativePath

                # Exact match
                if ($GroupPath.Equals(
                    $TimestampedPath,
                    [StringComparison]::OrdinalIgnoreCase
                )) {
                    return $true
                }

                # Timestamped path is the suffix of the
                # date-only path.
                #
                # Example:
                #
                # project1\readme.md
                #             readme.md
                #
                return $GroupPath.EndsWith(
                    "\" + $TimestampedPath,
                    [StringComparison]::OrdinalIgnoreCase
                )
            }
    )


    if ($MatchingGroups.Count -gt 0) {

        # Because DateOnlyGroups is sorted alphabetically,
        # this is deterministic.
        $SelectedGroup = $MatchingGroups[0]

        $AssignedTimestamped[$Timestamped.File.FullName] =
            $SelectedGroup.Name
    }
}


# ------------------------------------------------------------
# Build the final duplicate groups.
#
# Key = logical <basedir>\<subfolder>\<file> for date-only
#       files, or the selected date-only key for timestamped
#       files.
# ------------------------------------------------------------

$LogicalEntries = @()


# Add date-only files
foreach ($Entry in $DateOnlyFiles) {

    $LogicalEntries += [PSCustomObject]@{
        File         = $Entry.File
        LogicalPath  = $Entry.RelativePath
        RevisionDate = $Entry.RevisionDate
        RevisionDir  = $Entry.RevisionDir
        DateOnly     = $true
    }
}


# Add timestamped files that could be assigned to a
# date-only group.
foreach ($Entry in $TimestampedFiles) {

    $FileName = $Entry.File.FullName

    if ($AssignedTimestamped.ContainsKey($FileName)) {

        $LogicalPath = $AssignedTimestamped[$FileName]

        $LogicalEntries += [PSCustomObject]@{
            File         = $Entry.File
            LogicalPath  = $LogicalPath
            RevisionDate = $Entry.RevisionDate
            RevisionDir  = $Entry.RevisionDir
            DateOnly     = $false
        }
    }
}


# ------------------------------------------------------------
# Process groups containing date-only files.
# ------------------------------------------------------------

$LogicalGroups = @(
    $LogicalEntries |
        Group-Object LogicalPath
)


$DeletedCount = 0
$WouldDeleteCount = 0


foreach ($Group in $LogicalGroups) {

    if ($Group.Count -le $KeepCopies) {
        continue
    }


    # Oldest first.
    #
    # RevisionDate is derived from the revision directory,
    # not the filesystem's LastWriteTime.
    #
    $FilesToDelete = @(
        $Group.Group |
            Sort-Object RevisionDate |
            Select-Object -First (
                $Group.Count - $KeepCopies
            )
    )


    foreach ($Entry in $FilesToDelete) {

        if ($DryRun) {

            Write-Host "[DRY RUN] Would delete:" `
                -ForegroundColor Yellow

            Write-Host "           $($Entry.File.FullName)"
            Write-Host "           Logical path: $($Entry.LogicalPath)"
            Write-Host "           Revision:     $($Entry.RevisionDate)"
            Write-Host ""

            $WouldDeleteCount++
        }
        else {

            Write-Host "Deleting: $($Entry.File.FullName)" `
                -ForegroundColor Red

            Remove-Item `
                -LiteralPath $Entry.File.FullName `
                -Force

            $DeletedCount++
        }
    }
}


# ------------------------------------------------------------
# Timestamped files that were NOT assigned to a date-only
# group are handled separately.
#
# They are compared only with other timestamped files having
# exactly the same relative path.
# ------------------------------------------------------------

$UnassignedTimestamped = @(
    $TimestampedFiles |
        Where-Object {
            -not $AssignedTimestamped.ContainsKey(
                $_.File.FullName
            )
        }
)


$TimestampedGroups = @(
    $UnassignedTimestamped |
        Group-Object RelativePath
)


foreach ($Group in $TimestampedGroups) {

    if ($Group.Count -le $KeepCopies) {
        continue
    }


    $FilesToDelete = @(
        $Group.Group |
            Sort-Object RevisionDate |
            Select-Object -First (
                $Group.Count - $KeepCopies
            )
    )


    foreach ($Entry in $FilesToDelete) {

        if ($DryRun) {

            Write-Host "[DRY RUN] Would delete:" `
                -ForegroundColor Yellow

            Write-Host "           $($Entry.File.FullName)"
            Write-Host "           Logical path: $($Group.Name)"
            Write-Host "           Revision:     $($Entry.RevisionDate)"
            Write-Host ""

            $WouldDeleteCount++
        }
        else {

            Write-Host "Deleting: $($Entry.File.FullName)" `
                -ForegroundColor Red

            Remove-Item `
                -LiteralPath $Entry.File.FullName `
                -Force

            $DeletedCount++
        }
    }
}


# ------------------------------------------------------------
# Summary
# ------------------------------------------------------------

Write-Host ""
Write-Host "============================================"
Write-Host "Backup cleanup complete"
Write-Host ""

if ($DryRun) {

    Write-Host "DRY RUN: no files were deleted." `
        -ForegroundColor Yellow

    Write-Host "Files that would be deleted: $WouldDeleteCount"
    Write-Host ""
    Write-Host "To actually delete them, run:"
    Write-Host ""
    Write-Host "powershell.exe -File `"$PSCommandPath`" -Delete"
}
else {

    Write-Host "Files deleted: $DeletedCount" `
        -ForegroundColor Green
}

Write-Host "============================================"