How to bulk rename items in SharePoint Online and OneDrive for Business

In this article, we will examine some methods to bulk rename files in SharePoint Online/OneDrive for Business. The original query was about changing the names to lowercase, but we will also cover some more interesting scenarios, such as importing the names from a CSV file or leveraging a column/property. In terms of APIs, we will be using the Graph API (via the Graph SDK for PowerShell) as well as CSOM (via the PnP PowerShell module).

Bulk rename files to lowercase via the Graph SDK

Let’s start with a basic example – we have a bunch of files within a given library, and we want to change all their names to use lowercase. The Graph API handles this scenario via the PATCH item method, with Name being the only property we need to modify. The downside is being able to provide the correct identifiers for the site/library. In order to keep things simple, we’ll use a basic example of updating all items within a given Team site. Start by getting the URL of the site’s default library, by either copying it in the browser or via the GET /drive method (aka the Get-MgGroupDefaultDrive cmdlet):

Get-MgGroupDefaultDrive -GroupId 0a6e2870-bde9-4d29-987a-e6846f34ba41

Name      Id                                                                 DriveType       CreatedDateTime
----      --                                                                 ---------       ---------------
Документи b!3Y5k2T8JIE6Ii7FrIq807TyV1pbBVZdCsA_nctKVTjod2Vk6hfFeR4gEerOcdYn0 documentLibrary 15/06/24 23:58:54

The reason we want to work with the drive resource, and later on with driveItem objects, is because the rename operation is handled much easier, compared to using lists and listItems. The downside is that we don’t have a built-in cmdlet to fetch all items within a given drive. There are few ways to go around this, but in order to keep things simple, we can just use the /list endpoint to fetch the associated list, then iterate over each item in it. When using the Graph SDK for PowerShell, this can be achieved via the Get-MgDriveListItem cmdlet.

We also want to make sure to fetch the driveItem details for each list item, as we need the corresponding driveItemId for the rename operation. This can be achieved by leveraging the $expand query operator. In order to minimize the amount of data returned, we make sure to request just the properties we need, in both the “main” request and as part of the $expand query. Here’s an example:

#Get the group's default drive ("Shared Documents" library)
$drive = Get-MgGroupDefaultDrive -GroupId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

#Get all items within the default drive along with their driveItem facet
$items = Get-MgDriveListItem -DriveId $drive.id -ExpandProperty 'driveItem($select=id,Name,webUrl)' -Property Id,driveItem

After obtaining the list of items, we will use the Update-MgDriveItem cmdlet to change the Name property. The cmdlet even has a corresponding parameter, so we don’t have to deal with the usual JSON crap. Imagine that! Combining all of the above, we finally come up with the following code:

#Get the group's default drive ("Shared Documents" library)
$drive = Get-MgGroupDefaultDrive -GroupId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

#Get all items in the drive
$items = Get-MgDriveListItem -DriveId $drive.id -ExpandProperty 'driveItem($select=id,Name,webUrl)' -Property Id,driveItem

#Iterate over each item
foreach ($item in $items.driveItem) {
        #Check whether we need to rename it and skip if we don't
        if ($item.Name -ceq $item.Name.ToLower()) { continue }
        else {
            #Rename the item
            Update-MgDriveItem -DriveId $drive.Id -DriveItemId $item.Id -Name $item.Name.ToLower() #| Out-Null
        }
}

Overall, the above code is quite simple, but it gets the job done. It is a great testament of the benefits Graph API brings to the table, even if we account for the SDK’s oddities. This becomes even more obvious if we compare the code to equivalent code based on the PnP PowerShell cmdlets (see section below).

I did omit one important detail above, namely the permissions you need.  You can run the code above in either application or delegate context, the latter mandating that the current user has sufficient permissions on the corresponding site as well. For the Graph API side of things, you will need Files.ReadWrite or equivalent. Remember that you can scope both application and delegate permissions to specific site, library or even individual items, as we discussed previously.

Another thing to note is that the above example (as well as the other examples in the article) use a Microsoft 365 Group’s site as the location. Graph makes it possible to treat all sites in a similar manner, including the ones corresponding to a person’s OneDrive for Business. There are some subtle differences, but in most cases it should be possible to leverage the same GET /drive method we used in the example above to fetch the content of the default “Shared documents” library. That said, there are scenarios where you want to go beyond the default drive, or want to cover only a subset of the items in it… but we’ll keep it simple here.

Bulk rename files via CSV and the Graph SDK

As another example, let’s consider a scenario where we have a CSV file with “mapping” of existing file names and the desired one. This is a generic scenario where we’re offloading the work of coming up with the new names to some different process or user and we’re just acting on the provided CSV. An important thing to remember for such scenario is that it is essentially a one-time operation, as the input becomes invalid after the file is processed. Well, assuming matching items were found and were successfully renamed. The point is, it is much preferred to use immutable identifiers instead, such as the item’s (list)ID or its UniqueId.

Anyway, the code itself remains largely unchanged. We still need to fetch the drive and relevant items therein. While we can fetch and rename items one by one, based on the identifier provided via the CSV input, it might be better to just fetch all the items within the drive and filter out the ones we don’t care about. Your mileage will vary, depending on the number of items in the drive and the number of renames we need to perform.

The sample code below assumes that the CSV file (blabla.csv) has two columns: Name, to designate the current name of the item, and NewName, to designate the desired name. As mentioned above, it’s safer/better to use a unique identifier instead of the name, but for the purposes of our example, this should do. So, after fetching the set of items in the drive and the set of items to rename via the CSV, we do some basic string comparison and we skip any items for which the current name is not in the supplied CSV. Then, it’s business as usual, with just updating the replace value to match the one in the CSV.

#Get the group's default drive ("Shared Documents" library)
$drive = Get-MgGroupDefaultDrive -GroupId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
 
#Get all items in the drive
$items = Get-MgDriveListItem -DriveId $drive.id -ExpandProperty 'driveItem($select=id,Name,webUrl)' -Property Id,driveItem

#Load the CSV file
$itemsToRename = Import-CSV blabla.csv
 
#Iterate over each item
foreach ($item in $items.driveItem) {
    #Check whether we need to rename it and skip if we don't
    if ($item.Name -notin $itemsToRename.Name) { continue }

    #Set the new name
    $NewName = ($itemsToRename | ? {$_.Name -eq $item.Name}).NewName
    if (!$NewName -or ($NewName -eq $item.Name) -or ($NewName.count -gt 1)) { continue }
    else {
        #Rename the item
        Update-MgDriveItem -DriveId $drive.Id -DriveItemId $item.Id -Name $NewName #| Out-Null
    }
}

While renaming files via CSV should address any fancy scenarios, the manual steps involved in preparing the CSV file itself might be an issue if you are looking for full automated solution.

Bulk rename files by leveraging metadata

In some cases, it might be possible (or even necessary) to leverage metadata for the rename operations. Here, we use a broad definition for metadata, which includes any driveItem/listItem property we can get our hands on, such as the item’s creation and/or modification dates, author, managed metadata or any custom property. As a simple example, let’s take all items that have been stamped with a specific sensitivity label, say the Top secret one, and prepend their names with [Secret].

Out of the box, we can leverage the _DisplayName field to surface the sensitivity label assigned to the item, and we can even filter based on the value, including null values to give us all items with labels. Here’s an example:

Get-MgDriveListItem -DriveId $drive.id -ExpandProperty 'driveItem($select=id,Name,webUrl),fields($select=_DisplayName)' -Filter 'fields/_DisplayName ne null'

$items | select Id,@{n="Name";e={$_.DriveItem.Name}},@{n="Label";e={$_.Fields.AdditionalProperties["_DisplayName"]}}

Id Name          Label
-- ----          -----
2  book.xlsx     Top secret
12 Document.docx Top secret

In effect, we can use the following code that leverages both a driveItem property and listItem field for the task at hand. As you can leverage pretty much every other piece of metadata available on the item(s) in this manner, the possibilities are just endless. Without further ado, here is a sample code that renames files based on the presence of specific sensitivity label:

#Get the group's default drive ("Shared Documents" library)
$drive = Get-MgGroupDefaultDrive -GroupId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

#Get all items in the drive that have a sensitivity label assigned
$items = Get-MgDriveListItem -DriveId $drive.id -ExpandProperty 'driveItem($select=id,Name,webUrl),fields($select=_DisplayName)' -Filter 'fields/_DisplayName ne null'
 
#Iterate over each item
foreach ($item in $items) {
    #Check whether we need to rename it and skip otherwise
    if ($item.Fields.AdditionalProperties["_DisplayName"] -ne 'Top secret') { continue }
 
    #Set the new name
    else {
        $NewName = "[Secret]" + $item.DriveItem.Name
        Update-MgDriveItem -DriveId $drive.Id -DriveItemId $item.DriveItem.Id -Name $NewName #| Out-Null
    }
}

Bulk rename files to lowercase via PnP PowerShell

So, how would a similar code based on PnP PowerShell cmdlets look like? Much like in the Graph scenario, a single cmdlet is enough to list all items, the biggest difference being how we fetch the relevant properties. In the below example, we leverage the -Fields parameter to fetch just the relative URL for the items, as well as its name. The Rename-* cmdlets work just fine with the relative values, so for this basic example we do not need anything else. We do need to check the item type however, as we have separate cmdlets to run against folders and files, Rename-PnPFolder and Rename-PnPFile, respectively. This can be done by leveraging the FSObjType field, returned by default.

The other difference is that we can query the list/library by its name, which apparently is needed for our scenario, as filtering on BaseTemplate/BaseType or other builtin properties will return multiple matching lists. I’m not much of a PnP guy, so perhaps there’s a better way to fetch the default “Shared documents” library, without hardcoding its name. Anyway, one last difference is how we handle permissions, and connectivity in general. Those depend on the credentials and the application values you pass to the Connect-PnPOnline cmdlet, as the default multi-tenant PnP app is no more. In other words, make sure to replace the connectivity line in the sample code below with your preferred method!

#Open a PnP connection
Connect-PnPOnline -Url https://tenant.sharepoint.com/sites/example_site -Interactive -ClientId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

#Get the "Shared Documents" library
Get-PnPList -Identity "Shared Documents" -ThrowExceptionIfListNotFound

#Get all items
$items = Get-PnPListItem -List "Shared Documents" -Fields FileRef,FileLeafRef

#Iterate over each item
foreach ($item in $items.FieldValues) {
    #Check whether we need to rename it and skip if we don't
    if ($item.FileLeafRef -ceq $item.FileLeafRef.ToLower()) { continue }
    else {
        #Rename the item depending on its type
        if ($item.FSObjType -eq 0) { Rename-PnPFile -ServerRelativeUrl $item.FileRef -TargetFileName $item.FileLeafRef.ToLower() -Force }
        elseif ($item.FSObjType -eq 1) { Rename-PnPFolder -Folder $item.FileRef -TargetFolderName $item.FileLeafRef.ToLower() }
        else { continue } #Just in case
    }
}

One thing to note here is that unlike the Graph-based examples, we get no output when running the above code. If you prefer to see such, you will have to amend the code. In contrast, if you prefer no output for the Graph code samples as well, you can uncomment the Out-Null cmdlet I’ve left in there. It goes without saying that the above examples just that, examples. If you ever have the need to run similar code in a real environment, you should make sure to add proper error handling and logging capabilities, at minimum.

We can of course replicate the other examples as well, but that’s an exercise I will leave to the reader 🙂

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading