Moving a Folder to Inactive?

The scenario: HR wants to have a folder dedicated to employee files. Each employee has their own folder containing the files. Once the employee is no longer employed we would like to move their folder to an “Inactive” folder.

With our current workflow process the files have been successfully moved to a new folder in a new location, but the original folder is still there. Is there any way to delete the original folder, move over the original folder in its entirety, or is there a better suggested process for this?

Thanks.

You can do this in ECM UI by drag/drop the folder into another folder - but there is no way to do this via the workflow steps that I can find, but if you can (are willing) write an API/REST call, there is an API service for ECM called MoveFolder.
Reference for https://cloudbeta.docstar.com/EclipseServer/AstriaV2/Folder.svc/restssl/MoveFolder

EDIT - See solution code below

2 Likes

I didn’t realize they would expose a folder move through the API but not the workflows. While it seems strange to me to do it that way I’ve done enough work with REST that I think this is the best solution I have been able to consider.

Thank you very much!

1 Like

When you get it working, please come back and post. Our small ECM community is always looking for ways to get things done :slight_smile:

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.

1 Like

To be clear, the calling code is a PS script, but where are the functions code stored? I’m no PS guru - I’m seeing how PS is calling “LogInAccount” but not where it is calling it from.

I made a couple mistakes sanitizing it for posting, so those will be fixed soon if anyone notices anything egregious.

To your point, maybe I shouldn’t have called it “Calling code”, naming isn’t my strong suit. Every code block (the ones that start with “function”) after the first two are also included in the script, making one big script. There are different ways to do it in PowerShell but I kept it basic.

1 Like

Would it be easier to have an Employee folder structure

In workflow, use remove from all folders to change the user from one folder to another. Next action have an Add to Folder task.
The workflow may need to not end to use this functionality, but may be simpler and allow for the user record to move from Folder to Folder.

Company\Inactive<InsertName>
Company\Active<InsertName>