I finally got it finished up and ran last night. It worked for us, but I can’t promise it will suit someone else’s needs exactly. Either way, here’s the layout for what I created.
To reiterate, the goal was for a folder to be moved if an employee was considered inactive. HR would come into DocStar and run a workflow on the employee’s documents to set them to inactive and change permissions on their documents. Optimally, the workflow would have moved the folder afterwards but we couldn’t find a way until @MikeGross’s alternate suggestion. Using PowerShell and calling the API we will run it nightly using Power Automate to move any employee folders that still need moved, as it does not need to be done immediately, but can be triggered manually if required.
What our folder structure looks like:
Before:
Root Folder (Personnel)
Employee Type 1
Austin
Documents (with "zInactiveEmployee" flag set on all documents ran from workflow)
Boston
Documents
Employee Type 2
Costin
Employee Type 3
Flossin
Documents (with "zInactiveEmployee" flag set on all documents ran from workflow)
Josslyn
Inactive Employees
Employee Type 1
Employee Type 2
Employee Type 3
After:
Root Folder (Personnel)
Employee Type 1
Boston
Documents
Employee Type 2
Costin
Employee Type 3
Josslyn
Inactive Employees
Employee Type 1
Austin
Documents
Employee Type 2
Employee Type 3
Flossin
Documents
We first call their API (HostingV2/User.svc/rest/LogIn) for a token and pass that through to our calls.
function LogAccountIn($user, $password)
{
$body = @"
{
`"Password`":`"$($password)`",
`"ProxyLogin`": false,
`"ProxyOnBehalfOf`": `"00000000-0000-0000-0000-000000000000`",
`"Username`":`"$($user)`"
}
"@
$response = Invoke-RestMethod 'http://$($rootURL)/HostingV2/User.svc/rest/LogIn' -Method 'POST' -Headers $headers -Body $body
$response = $response | ConvertTo-Json | ConvertFrom-Json
return $response.Result.Token
}
Next we make another call (AstriaV2/Search.svc/rest/Search) and use that for subsequent searching through our “directory”.
function SearchByFolderId($folderID)
{
$body = @"
{
`"MaxRows`": `"25`",
`"IncludeFolders`": true,
`"IncludeInboxes`": false,
`"IncludeDocuments`": true,
`"IncludePackages`": true,
`"ContentTypeId`": null,
`"ContentTypeTitle`": `"`",
`"InboxId`": null,
`"FolderId`": `"$($folderID)`",
`"ContainerName`": `"REPLACE`", #root folder name. I hard-coded, could be dynamic if necessary
`"Start`": 0,
`"TextCriteria`": `"`",
`"FieldedCriteria`": [
{
`"Concatenation`": 1,
`"GroupConcatenation`": 1
}
],
`"SortBy`": `"title`",
`"SortOrder`": `"desc`",
`"refreshSearch`": false,
`"DocumentRetrieveLimit`": null,
`"PredefinedSearch`": 0,
`"IncludeSubFolders`": true,
`"IncludedFolderIds`": [],
`"IncludeColumn`": false,
`"Columns`": null,
`"LegacyTextParser`": true,
`"Name`": null,
`"PassThroughPaging`": false,
`"SaveAsRecent`": false,
`"WholeStringMatch`": false
}
"@
$response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Search.svc/rest/Search' -Method 'POST' -Headers $headers -Body $body
$results = ($response | ConvertTo-Json -Depth 8 | ConvertFrom-Json).Result.Results
return $results
}
For each one that matches our requirements, we move the folder (AstriaV2/Folder.svc/rest/MoveFolder) to the “Inactive Employees”/* folder.
function MoveFolder($employeeFolderId, $inactiveFolderId)
{
$body = @"
{
`"Id`":`"$($employeeFolderId)`",
`"NewRootId`":`"$($inactiveFolderId)`",
`"Title`":`"`"
}
"@
$result = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/MoveFolder' -Method 'POST' -Headers $headers -Body $body
}
Additionally, we call (AstriaV2/Folder.svc/rest/GetSecurityInformation) replace the SecurityClassID and update it with (AstriaV2/Folder.svc/rest/SetSecurityInformation).
function GetSecurityInformation($folder)
{
$body = @"
`"$($folder)`"
"@
$response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/GetSecurityInformation' -Method 'POST' -Headers $headers -Body $body
$results = ($response | ConvertTo-Json -Depth 8 | ConvertFrom-Json).Result.
return $results
}
function SetSecurityInformation($folder, $site)
{
$securityResult = GetSecurityInformation -folder $folder
$securityClassID = switch ($site)
{
#REPLACE comment with values hard-coded with securityClassIDs
# 'ThisSite' { "REPLACE GUID" }
}
$securityResult.SecurityClassId = $securityClassID
$body = $securityResult| ConvertTo-Json
$response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/SetSecurityInformation' -Method 'POST' -Headers $headers -Body $body
}
The calling code:
###REPLACE VALUES
$user = "" #Docstar Username
$password = "" #Docstar Password
$rootURL = "" #URL Path to DocStar
$rootFolder = "" #guid of base directory folder, in our instance: Personnel
$inactiveFolderName = "Inactive Employees" #change to name of inactive folder
$inactiveFlag = "zInactiveEmployee" #name of flag being detected to know if the folder needs to be moved
###REPLACE VALUES
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")
$token = LogAccountIn -user $user -password $password
$headers.Add("ds-token", $token)
$results = SearchByFolderId -folderID $rootFolder
$rootSubfolders = $results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $rootFolder) -and $_.Title -ne $inactiveFolderName} | ForEach-Object {
[PSCustomObject]@{
ID = $_.ID
Title = $_.Title
}
}
$inactiveEmployeesFolderID = ($results | Where-Object {$_.Title -eq $inactiveFolderName} | Select-Object Id).Id
$inactiveEmployeesSubFolders = $results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $inactiveEmployeesFolderID)} | ForEach-Object {
[PSCustomObject]@{
ID = $_.ID
Title = $_.Title
}
}
$employeeFolders = foreach($i in $rootSubfolders)
{
$results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $i.ID)}| ForEach-Object {
[PSCustomObject]@{
ParentID = ($_.DynamicFields | Where-Object {$_.Key -eq "DfFolderId"} | Select-Object Value).Value
ID = $_.Id
}
}
}
$oldToNewFolderID = foreach($i in $inactiveEmployeesSubFolders)
{
foreach($j in $rootSubfolders)
{
if ($i.Title -eq $j.Title)
{
[PSCustomObject]@{
OldID = $j.ID
NewID = $i.ID
Title = $i.Title
}
}
}
}
$foldersToMove = $results | Where-Object {$_.DynamicFields.Key -eq $inactiveFlag -and $_.DynamicFields.Value -eq $true} | ForEach-Object {
[PSCustomObject]@{
ID = ($_.DynamicFields | Where-Object {$_.Key -eq "DfFolderId"} | Select-Object Value).Value
}
}
foreach ($i in (($foldersToMove).ID | Sort-Object | Get-Unique))
{
$employeeFolderParentID = ($employeeFolders | Where-Object {$_.ID -eq $i} | Select-Object $_.ParentID).ParentID
if ($employeeFolderParentID -eq $null)
{
continue
}
#Write-Output "Folder to move - $($i)"
$inactiveFolderID = ($oldToNewFolderID | Where-Object {$_.OldID -eq $employeeFolderParentID} | Select-Object $_.NewID).NewID
#$site = ($oldToNewFolderID | Where-Object {$_.OldID -eq $employeeFolderParentID} | Select-Object $_.Title).Title
#$site = $site.Split(" ")[0]
#Write-Output "Moving folder $($i) from $($employeeFolderParentID) to $($inactiveFolderId)"
MoveFolder -employeeFolderId $i -inactiveFolderId $inactiveFolderID
SetSecurityInformation -folder $i #-site $site
}
“$Site” has been commented out as it might not suit someone else’s needs, but we are controlling our folders by site as well, such as “$Site Hourly” requiring additional logic on where to move the inactive employee’s folder.
I’m sure the code may look clunky to some, but it’s the solution I came up with. There are a few “REPLACE” statements in the code for things that need changed if you plan on using any part of this. Additionally, the Write-Outputs at the end are optional but help when diagnosing what is happening.