Поиск по сайту:

Объединение дублирующихся папок в SharePoint с помощью PowerShell


Один из читателей этого блога связался со мной и спросил, знаю ли я способ объединить дублирующиеся папки в SharePoint Online. Пользователи уже работали в дублирующихся папках, так что нам нужно было не только объединить папки, но и сохранить последний измененный файл.

Все дублирующие файлы или папки имели один и тот же шаблон, (1), в своем имени файла. Это сделало поиск файлов и папок и их объединение с оригинальным местоположением действительно простым.

Если вам интересно, что произошло; пользователь удалил файлы локально, не остановив синхронизацию сначала. Из-за большого количества удаленных файлов (более 750 000) они попросили Microsoft восстановить их, и компания это сделала.

Только из-за ошибки связи восстановительная операция была выполнена в два этапа, что привело к дублированию файлов. Microsoft не смогли помочь с очисткой, поэтому я вмешался.

Это одна из тех ситуаций, когда решение для резервного копирования от третьей стороны действительно может быть спасением. Я уже писал об этом ранее; Нужна ли вам решение для резервного копирования для Microsoft 365. Как вы можете видеть в этом случае, файлы можно восстановить, но наличие решения для резервного копирования значительно упростило бы процесс восстановления.

Поиск дублирующихся элементов

Объединение дублирующих папок и файлов потребует несколько шагов. Дублирующие папки находятся не только на корневом уровне библиотеки документов, но могут также находиться на глубине трех уровней в подпапке. Поэтому нам нужно работать рекурсивно через все папки, ищя элементы с (1) в названии.

Я разбил скрипт на несколько шагов, каждый из которых переведен в свою функцию:

  • Поиск дубликатов файлов и папок
  • Создайте целевой путь (оригинальное местоположение)
  • Переместите содержимое папки
  • Сравнить даты файлов
  • Переместите один объект

Подключитесь к сайту SharePoint

Прежде чем мы сможем что-либо сделать, нам нужно подключиться к сайту SharePoint. Мы можем сделать это просто с помощью PnPOnline, и я использую переключатель веб-входа здесь, чтобы мы могли использовать наш обычный вход с MFA.

В конце статьи я покажу полный сценарий.

SharePoint url
$sharepointUrl = 'https://a-d.sharepoint.com/'

Site
$site = 'sites/lab01'

Library name
$libraryName = 'Duplicates'

Login 
$url = $sharepointUrl+ '/' + $site
Connect-PnPOnline -Url $url -UseWebLogin

Поиск дублирующихся файлов

Поскольку нам также необходимо обрабатывать подпапки, здесь трюк заключается в том, чтобы пройти через папки рекурсивно. Мы используем функцию Get-PnPFolderItem, но затем в рекурсивном порядке. Эта функция основана на скрипте Джоша Эйнштейна, который вы можете найти здесь.

Recursively calls Get-PnpFolderItem for a given Document Library
Based on: https://gist.github.com/josheinstein/3ace0c9f8e25d07583ceb57d13f71b2e

Function Get-PnpFolderItemRecursively($FolderSiteRelativeUrl) {
    
    # Get all items
    $items = @(Get-PnPFolderItem -FolderSiteRelativeUrl $FolderSiteRelativeUrl)

    Foreach ($item in $items) {

        # Strip the Site URL of the item path because Get-PnpFolderItem wants it
        # to be relative to the site, not an absolute path.

        $itemPath = $item.ServerRelativeUrl -replace "^$(([Uri]$item.Context.Url).AbsolutePath)/",''
 
        # Check if the item is a folder
        If ($item -is [Microsoft.SharePoint.Client.Folder]) 
        {
            
            # Check if the folder name contains (1) on the end
            # If - if the folder name contains a (1) on the end, then it's a duplicate folder that we need to move or merge
            # Else - if the folder doesn't contain (1), then we open the folder and search through the next level

            if ($item.name  -like "*(1)") 
            {
         
                # Duplicate folder found
                Write-Host " - Duplicatie folder found: " $itemPath -ForegroundColor Yellow
            
                # Move the content of the folder to the original location
                Move-FolderItemsRecursively($itemPath)
            }
            else
            {
                # Is doesn't contain (1), but it's a folder, search through the next level by recursing into this function.
                Get-PnpFolderItemRecursively $itemPath
            }
        }
        else
        {
            # Item is a file
            # Check if items name contains a (1), if true, move the file

            if ($item.name  -like "*(1)") 
            {
                $targetPath = Create-TargetPath -itemPath $itemPath -targetPath $item["FileRef"].trim("*(1)") -relativePath $relativePath

                Write-Host $newTargetPath;

                Move-CustomItem -SiteRelativeUrl $itemPath -targetPath $targetPath -item $item
            }
            # Else skip to next
        }
    }
}

Итак, для каждого элемента мы проверяем, является ли он папкой. Если это папка, мы проверяем имя папки, содержит ли оно (1). Если да, то мы перемещаем содержимое папки, если нет, мы "открываем" папку и проходим через содержимое.

Если это не папка, а файл, то снова проверяем имя. Если оно содержит (1), то мы собираемся переместить файл, а иначе ничего не делаем.

Создание целевого пути

Если мы найдём дубликат, нам нужно создать оригинальный путь из пути элемента. Также необходимо проверить, существует ли оригинальная папка, а если нет, то восстановить оригинальную папку.

Function Create-TargetPath {
    [CmdletBinding()]
    param(
         [parameter (Mandatory=$true)]
         $itemPath,

         [parameter (Mandatory=$true)]
         $item,

         [parameter (Mandatory=$false)]
         $relativePath
     )

    process
	{
        # Build new path
        $path = $itemPath.replace($item.name,'') 
        $targetPath = "/" + $site + "/" + $path + $item.name

        if ($whatIf -ne $true)
        {
            # Check if target folder exists, create if necessary
            Write-host ' - Check if target folder exists' $path.replace('(1)', '') -BackgroundColor DarkMagenta;
            $result = Resolve-PnPFolder -SiteRelativePath $path.replace('(1)', '') -ErrorAction SilentlyContinue
        }
        else{
            Write-host ' - Create target folder if it does not exists' $path.replace('(1)', '') -BackgroundColor DarkMagenta;
        }

        Write-Output $targetPath.replace('(1)', '')
    }
}

Что вы увидите в каждой функции, так это то, что я использую переменную $whatIf. В начале скрипта я объявил эту переменную. Это позволяет мне тестировать скрипт, не перемещая и не создавая никаких файлов или папок.

Большинство функций PnP действительно поддерживают параметр WhatIf, но, например, Resolve-PnPFolder не поддерживает его. Таким образом, я могу просто протестировать это, записывая в консоль, что сделает скрипт.

Функция create-targetPath будет воссоздавать путь, проверять, существует ли папка, и если нет, воссоздавать ее с помощью функции Resolve-PnPfolder.

Переместите папку и подпапки с помощью PowerShell

Когда мы нашли дубликат папки, мы хотим переместить папку и все её содержимое (включая подпапки) в оригинальное местоположение. Для каждой подпапки нам также необходимо создать целевой путь.

Function Move-FolderItemsRecursively($FolderSiteRelativeUrl) {

    # Get all items in this sub folder
    $items = @(Get-PnPFolderItem -FolderSiteRelativeUrl $FolderSiteRelativeUrl)

    foreach ($item in $items) {

        # Strip the Site URL off the item path, because Get-PnpFolderItem wants it
        # to be relative to the site, not an absolute path.
        
        $itemPath = $item.ServerRelativeUrl -replace "^$(([Uri]$item.Context.Url).AbsolutePath)/",''

        # If this is a directory, recurse into this function.
        # Otherwise, build target path and move file

        if ($item -is [Microsoft.SharePoint.Client.Folder]) 
        {
            Move-FolderItemsRecursively $itemPath
        }
        else 
        {
            $targetPath = Create-TargetPath -itemPath $itemPath -item $item
            
            Move-CustomItem -SiteRelativeUrl $itemPath -targetPath $targetPath -item $item
        }
    }
}

Сравнение дат файлов в SharePoint Online и перемещение файлов

Итак, мы переходим к основной части — перемещению самих файлов. Прежде чем переместить файл, нам нужно проверить, существует ли файл в исходном местоположении. Если он существует, необходимо сравнить даты файлов, в этом случае мы хотим оставить последний изменённый файл.

Move file to original folder
Function Move-CustomItem  {
    [CmdletBinding()]
    param(
         [parameter (Mandatory=$true)]
         $siteRelativeUrl,

         [parameter (Mandatory=$true)]
         $targetPath,

         [parameter (Mandatory=$true)]
         $item
     )

    process
	{
        $moveFile = Compare-FileDates -sourceFilePath $siteRelativeUrl -targetFilePath $targetPath;
		$global:moveLimitCounter++

        if ($moveFile -eq $true) 
        {

			if ($moveLimitCounter -eq $moveLimit)
			{
				Write-Warning 'Move limit reached'
				exit;	
			}

            if ($whatIf -ne $true)
            {
				# Move the file
				Write-host '   - Move item to' $targetPath -BackgroundColor DarkYellow;
				Move-PnPFile -SiteRelativeUrl $siteRelativeUrl -TargetUrl $targetPath -OverwriteIfAlreadyExists -Force:$force
				Write-Host "`r`n"
				
            }
            else
            {
                Write-host '   - Move file from' $siteRelativeUrl -BackgroundColor DarkCyan
				Write-host '     to' $targetPath -BackgroundColor DarkCyan
				Write-Host "`r`n"
            }
        }
    }    
}

Я добавил счетчик, который будет считать количество перемещенных файлов. Когда вы будете тестировать такой скрипт, вы, возможно, захотите сначала переместить всего несколько файлов, а затем проверить результаты, прежде чем продолжать.

Check if the file already exists in the target location
If the file exists, we need to compare the dates to keep the latest files

Function Compare-FileDates () 
{
    [CmdletBinding()]
    param(
         [parameter (Mandatory=$true)]
         $targetFilePath,

         [parameter (Mandatory=$true)]
         $sourceFilePath
     )

    $targetFileExists = Get-PnPFile -Url $targetFilePath -ErrorAction SilentlyContinue
    
    If($targetFileExists)
    {
        $sourceFile = Get-PnPFile -Url $sourceFilePath -AsListItem
        $targetFile = Get-PnPFile -Url $targetFilePath -AsListItem

        $sourceFileDate = Get-date $sourceFile['Modified']
        $targetFileDate = Get-date $targetFile['Modified']

        write-host ' - Comparing files dates: duplicate file: '$sourceFileDate 'original file: '$targetFileDate

        # Check if the source file is newer then the target file
        If ($sourceFile['Modified'] -gt $targetFile['Modified']) 
        {
            write-host '    - Duplicate file is newer, move the file' -BackgroundColor DarkGreen
            write-output $true
        }
        else
        {
			# Remove file
			if ($whatIf -ne $true)
            {
				Write-host '    - Target file is newer. Removing duplicate file' -BackgroundColor DarkRed
				Write-Host "`r`n"
				Remove-PnPFile -SiteRelativeUrl $sourceFilePath -Recycle -Force:$force
			}
			else
			{
				Write-Host 'Remove file' $sourceFilePath  -ForegroundColor Red
				Write-Host "`r`n"
			}
            write-output $false
        }
    }
    else
    {
        # Target file doesn't exists
        Write-host ' - Target file does not exist' -BackgroundColor DarkGreen
        Write-Output $true
    }
}

Итак, мы сравниваем даты, и если исходный файл новее, то мы удаляем дубликат файла. В функции удаления файла я добавил переключатель -Recycle, так что файл будет перемещен в корзину. Вы можете удалить этот переключатель, если хотите навсегда удалить файлы.

Завершение

Хорошо знать, что у вас могут оказаться пустые папки в библиотеке. Я использовал отдельный скрипт, который вы можете запустить, чтобы очистить пустые папки. Полный скрипт вы можете найти здесь на моем GitHub.

Всегда тестируйте такие скрипты на тестовом сайте SharePoint с некоторыми тестовыми файлами. Убедитесь, что вы установили whatif в $true и переменную force в $false при запуске.

Если у вас есть вопросы, просто оставьте комментарий ниже.