Dienstag, 13. August 2019

LightCore bindings für Quartz.Net und Topshelf

Schon vor etwas längerer Zeit habe ich Bindings erstellt um zum einen Topshelf und zum anderen Quartz.Net "besser" mit LightCore verwenden zu können.

LightCore ist ein sehr schneller und einfach zu verwendender DI-Container.

Topshelf und Quarz.Net können beide sehr gut mit DI-Containern verwendet werden, allerdings gab es keine Anbindung an LightCore.

Wenn Quartz.LightCore und Topshelf.LightCore verwendet werden sieht der code so aus:

// setup LightCore
var builder = new ContainerBuilder();
/* some fancy setup here */
var container = builder.Build();

// setup Quartz
var scheduler = await new StdSchedulerFactory()
    .GetScheduler()
    .UseLightCoreResolverJobFacotry(container);
scheduler.ScheduleJob(
    JobBuilder.Create().Build(),
    TriggerBuilder.Create().StartNow().Build());

// setup Topshelf
var host = HostFactory.Run(x =>
{
    x.UseLightCore(container); // Enable LightCore
    x.Service(s =>
    {
        s.ConstructUsingLightCore(); // Construct service using LightCore
        s.WhenStarted(tc => tc.Start());
        s.WhenStopped(tc => tc.Stop());
        /* more Topshelf code... */
    });
});

Die NuGet-Pakete finden sich unter Install-Package Topshelf.LightCore und Install-Package Quartz.LightCore

Montag, 15. April 2019

Rekursives Hochladen von Ordnern in SharePoint mit PNP

Die Frage heute beim Kunden war: Kann ich "schnell" und mit PNP (d.h. ohne server-code) einen kompletten Ordner - mit Unterordnern - in eine SharePoint-Bibliothek hochladen?

Die Antwort ist ja, aber... (nicht "einfach so")
Ich habe hier ein Skript vorbereitet, dass die Arbeit übernimmt:

  • Erstellen eines Ordners im SharePoint, falls gewünscht
  • Hochladen aller Dateien aus dem Quell-Ordner
  • Rekursive Verarbeitung aller Unterverzeichnisse
<#  
.SYNOPSIS  
    Uploads a Folder, including all Files and subfolders
.Notes
    This Cmdlet assumes you have PNP installed and Connect-PNPOnline was issued before this command.
.Example
    Add-PNPFolderRecursive -Path C:\temp -Folder /SiteAssets -NoRootFolderCreation
    Uploads all of c:\temp to /SiteAssets
.Example
    Add-PNPFolderRecursive -Path C:\temp -Folder /SiteAssets
    Creates a "temp" folder in /SiteAssets, then uploads all of c:\temp to the newly created /SiteAssets/temp
#>
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true)]
    $Path,
    [Parameter(Position=1, 
        Mandatory=$true)]
    $Folder,
    [Parameter(Mandatory=$false)]
    [switch]$NoRootFolderCreation
)
$ErrorActionPreference="Stop"

$dstFolder = Get-PNPFolder $Folder
Write-Verbose "Acquired folder: $($dstFolder.ServerRelativeUrl)"

if(!$NoRootFolderCreation.IsPresent) {
    $folderName = Split-Path -Leaf $Path
    Write-Verbose "Creating target-folder $folderName"
    Add-PNPFolder -Name $folderName -Folder $dstFolder.ServerRelativeUrl
    $dstFolder = Get-PNPFolder "$($dstFolder.ServerRelativeUrl)/$($folderName)"
    Write-Verbose "Acquired folder: $($dstFolder.ServerRelativeUrl)"
}

# get all childs of "path" and upload the files...
$files = Get-ChildItem -File -Path $Path

$files | % {
    Write-Verbose "Uploading $($_.FullName)"
    Add-PNPFile -Path $_.FullName -Folder $dstFolder.ServerRelativeUrl | Out-Null
}

# recursive call subfolders
$foders = Get-ChildItem -Directory -Path $Path

$foders | %{
    Write-Verbose "Descending to Folder: $($_.FullName)"
    Invoke-Expression -Command ($PSCommandPath + " -Path $($_.FullName) -Folder $($dstFolder.ServerRelativeUrl) ")
}
Write-verbose "Done for $Path"

Achtung: Ich bin nicht sicher, wie sich das Skript verhält, wenn sehr viele und tiefe Ordnerstrukturen vorliegen - aufgrund der Rekursion im Skript selber vermute ich, dass die Performance ab einem bestimmten Punkt deutlich einbrechen wird.

Dienstag, 6. November 2018

IEnumerable-ToList() - alternative in PowerShell

Jeder weiß (oder lernt schnell), dass ein IEnumerable nicht verändert werden darf, während es durchlaufen wird.
Speziell im SharePoint-Bereich gilt also, wenn man das folgende in PowerShell versucht:

(Get-SPSite http://mein.sharepoint).AllWebs `
  | select -ExpandProperty Lists `
  | select -ExpandProperty Fields `
  | %{ $_.Update() }

Führt dies zu einer Menge von Fehlern die wie folgt aussehen:
An error occurred while enumerating through a collection: Collection was modified; enumeration operation may not execute.....

In C# würde ich einfach ToList() einstreuen. Dies würde dazu führen, dass eine flache Kopie des IEnumerable erstellt wird und in weiteren Verarbeitungen der Liste dieser Fehler nicht auftritt.
(Es führt natürlich auch dazu, dass der Enumerator an dieser Stelle vollständig durchlaufen wird, und man all die schönen Vorteile des IEnumerables (d.h. "Lazy Evaluation") verliert)

Eine schöne PowerShell-alternative zu ToList() ist im PowerShell Team Blog verfügbar:

function ToArray {
  begin {
    $output = @(); 
  }
  process {
    $output += $_; 
  }
  end {
    return ,$output; 
  }
}

Dadurch sieht der Aufruf dann wie folgt aus:

(Get-SPSite http://mein.sharepoint).AllWebs `
  | select -ExpandProperty Lists `
  | select -ExpandProperty Fields `
  | ToArray `
  | %{ $_.Update() }

und Funktioniert fehlerfrei.


Mittwoch, 15. August 2018

Papierkörbe einer WebApplication Auflisten & Löschen

​Zur "Bereinigung" einer WebApplication kann man dann- und wann mal die Paprierkörbe ansehen und-/oder löschen.

Ich habe da mal ein keines Skript dazu Vorbereitet:
<# 
.SYNOPSIS  
    List or Remove elements from RecycleBin(s) of a WebApplication 
.DESCRIPTION  
    This script lists- and optionally removes all items from all RecycleBins 
    of a WebApplication, including the End-Users (1st Stage) and 
    Administrators (2nd Stage) RecycleBins.
.NOTES  
    File Name  : Delete-Site-Recycle-Bin.ps1  
    Author     : Nils Andresen - nils.andresen@adesso.de    
.Example
    .\Empty-SPRecycleBin.ps1 -WebApp http://sp.dev/ -FirstStageCleanup RemovePermanent -SecondStageCleanup RemovePermanent
    Removes all deleted items (1st and 2nd stage) from all Sites/Webs of the WebApplication
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory=$True, HelpMessage = "Url to the WebApp")]
    [string]$WebApp,
 
    [Parameter(HelpMessage = "What to do with the 1st-stage Recycle Bins")]
    [ValidateSet("ListOnly", "MoveTo2nd", "RemovePermanent")]
    [string]$FirstStageCleanup = "ListOnly",
 
    [Parameter(HelpMessage = "What to do with the 2nd-stage Recycle Bins")]
    [ValidateSet("ListOnly", "RemovePermanent")]
    [string]$SecondStageCleanup = "ListOnly"
)
 
if((Get-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
}
 
$Global:TotalRemovedSize = 0;
 
function Format-ForPc {
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [int]$size
)
    if($size -lt 1MB) {
        return "{0:0.0#}KB" -f ($size / 1KB);
    } 
    if($size -lt 1GB) {
        return "{0:0.0#}MB" -f ($size / 1MB);
    } 
    if($size -lt 1TB) {
        return "{0:0.0#}GB" -f ($size / 1GB);
    } 
    return "{0:0.0#}TB" -f ($size / 1TB);
}
 
function Process-Web {
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [Microsoft.SharePoint.SPWeb[]]$web
)
Process {
    Write-Verbose "Accessing Web: $($web.Url)";
 
    if((-not $web.RecycleBinEnabled) -or ($web.RecycleBin.Count -lt 1)) {
        return;
    }
    $size = 0;
    $web.RecycleBin | %{ $size += $_.Size }
 
    Write-Output "Web $($web.Title) has $($web.RecycleBin.Count) items ($($size | Format-ForPc)) in Users-RecycleBin";          
         
    switch ($FirstStageCleanup) {
        "ListOnly" {
            $web.RecycleBin | %{ Write-Output "- $($_.ItemType):$($_.Title) ($($_.Size | Format-ForPc), Deleted by $($_.DeletedByName))" }
        }
        "RemovePermanent" {
            $web.RecycleBin.DeleteAll();
            $Global:TotalRemovedSize += $size;
            Write-Output "- Deleted permanently";
        }
        "MoveTo2nd" {
            $web.RecycleBin.MoveAllToSecondStage();
            Write-Output "- Moved to second stage";
        }
    }
}
}
 
function Process-Site {
[CmdletBinding()]
param(
    [Parameter(Position=0,
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [Microsoft.SharePoint.SPSite[]]$site
)
Process {
    Write-Verbose "Accessing Site: $($site.Url)";
 
    $secondStage = $site.RecycleBin | ? { $_.ItemState -eq [Microsoft.SharePoint.SPRecycleBinItemState]::SecondStageRecycleBin }
 
    $site.AllWebs | Process-Web
 
    if($secondStage.length -lt 1) {
        return;
    }
    $size = 0;
    $secondStage | %{ $size += $_.Size }
 
    Write-Output "Site $($site.Title) ($($site.Url)) has $($secondStage.Length) items ($($size | Format-ForPc)) in Admin-RecycleBin";          
 
    switch ($SecondStageCleanup) {
        "ListOnly" {
            $secondStage | %{ Write-Output "- $($_.ItemType):$($_.Title) ($($_.Size | Format-ForPc), Deleted by $($_.DeletedByName))" }
        }
        "RemovePermanent" {
            $secondStage | %{ $_.Delete(); }
            $Global:TotalRemovedSize += $size;
            Write-Output "- Deleted permanently";
        }
    }
}
}
 
 
$sa = Start-SPAssignment
$w = Get-SPWebApplication $WebApp -AssignmentCollection $sa;
$w.Sites | Process-Site;
Stop-SPAssignment $sa
if($SecondStageCleanup -eq "RemovePermanent" -and $FirstStageCleanup -eq "MoveTo2nd") {
    Write-Warning "The selected combination of removing from second stage and moving from first to second possibly leaves items undeleted."
}
if($Global:TotalRemovedSize -gt 0) {
    Write-Output "$($Global:TotalRemovedSize | Format-ForPc) were removed permanently.";
}

SharePoint: Auflisten aller Obejkte mit eigener Berechtigungszuweisung

​Die Frage meines Kunden letztens: "Wie kann ich alle Elemente (Listen, Items, etc.) auflisten, bei denen eigene berechtigungszuweisungen erfolgt sind?"

Meine schnelle Antwort: "Nicht möglich, OOTB!".

Mittlerweile weiß ich: In 2007 ging das - aber nur mit dem SharePoint Administrator Toolkit.

Ich habe - mit Inspiration von Mike Smith - ein Skript erstellt​, dass eben dies macht.

<#    
    .SYNOPSIS 
    List all items with broken inheritance
 
    .DESCRIPTION
    Lists all Webs, Lists, Folders and Items with broken inheritance.
    This script was heavily inspired by http://techtrainingnotes.blogspot.de/2014/07/sharepoint-powershellfind-all-broken.html
 
    .PARAMETER SiteCollectionUrl
    Url to SiteCollection
 
    .PARAMETER Site
    Site. Can be Piped in.
 
    .PARAMETER StopAtWeb
    Stop the search at Web-Level. Do not search Lists/Folders/Items.
 
    .PARAMETER StopAtList
    Stop the search at List-Level. Do not search Folders/Items.
 
    .PARAMETER StopAtFolder
    Stop the search at Folder-Level. Do not search Items.
 
    .INPUTS
    Site
 
    .OUTPUTS
    Grid of Securable | Item | Url | Parent
    where
     Securable is one of "Web", "List", "Folder" or "Item"
     Item is the Name or Title
     Url is the url to the item
     Parent is the url to the parent-item
 
 
    .EXAMPLE
    .\Get-SpBrokenInheritances.ps1 -SiteCollectionUrl http://my.lovely.site/
#>
[CmdletBinding()]
param (
    [Parameter(ParameterSetName='SiteByUrl')]
    [string]$SiteCollectionUrl,
     
    [Parameter(ParameterSetName='SiteByObject', ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPSite]$Site,
 
    [Parameter()]
    [switch]$StopAtWeb = $false,
 
    [Parameter()]
    [switch]$StopAtList = $false,
 
    [Parameter()]
    [switch]$StopAtFolder = $false
)
 
Set-StrictMode -Version Latest
$script:ErrorActionPreference = "Stop";
 
function Get-ParentUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPListItem]$Item
    )
    $List = $Item.ParentList;
     
    if ($List.BaseType -eq [Microsoft.SharePoint.SPBaseType]::DocumentLibrary) {
        return "$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.File.ParentFolder.Url)";
    } else {
        # SPListItem.Url looks like ////_.000 - I have no idea how to get the folder Url "correctly"
        $FolderUrl = $Item.Url.Substring(0, $Item.Url.LastIndexOf("/"));
        return "$($List.ParentWeb.ServerRelativeUrl)/$FolderUrl";
    }
}
 
function Get-Url {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPListItem]$Item
    )
    $List = $Item.ParentList;
    if ($List.BaseType -eq [Microsoft.SharePoint.SPBaseType]::DocumentLibrary) {
        return "$($List.ParentWeb.ServerRelativeUrl)/$($Item.Url)";
    } else {
        # e.g. /blubb/Lists/Test12/DispForm.aspx?ID=1
        return "$($Item.ParentList.DefaultDisplayFormUrl)?ID=$($Item.Id)";
    }
}
 
function Process-Lists {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPList[]]$Lists
    )
    $Folders = $Lists | Select -ExpandProperty Folders;
    $Folders | ? { $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"Folder"}}, 
            @{Label="Item"; Expression={$_.Title}}, 
            @{Label="Url"; Expression={"$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.Url)"}},
            @{Label="Parent"; Expression={"$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.ParentList.RootFolder.Url)"}} | Write-Output
 
    if($StopAtFolder) {
        return;
    }
    $Items = $Lists | Select -ExpandProperty Items;
    $Items | ? { $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"Item"}}, 
            @{Label="Item"; Expression={$_.Name}}, 
            @{Label="Url"; Expression={Get-Url -Item $_ }},
            @{Label="Parent"; Expression={Get-ParentUrl -Item $_ }} | Write-Output
}
 
function Process-Webs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $Webs
    )
    $Lists = $Webs | Select -ExpandProperty Lists | ? { $_.EntityTypeName -ne "PublishedFeedList" -and -not $_.Hidden }
    $Lists | ?{ $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"List"}}, 
            @{Label="Item"; Expression={$_.Title}}, 
            @{Label="Url"; Expression={"$($_.ParentWeb.ServerRelativeUrl)/$($_.RootFolder.Url)"}},
            @{Label="Parent"; Expression={$_.ParentWebUrl}} | Write-Output
   
    if($StopAtList) {
        return;
    }
    Process-Lists -Lists $Lists
}
 
function Process-Site {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPSite]$Site
    )
    Write-Verbose "Process-Site $($Site.RootWeb.Title)";
    $WebGc = Start-SPAssignment;
    try {
        $Webs = $Site | Get-SPWeb -AssignmentCollection $WebGc -Limit All;
        $Webs | ?{ $_.HasUniquePerm -and $_.ParentWeb -ne $Null } | 
            Select @{Label="Securable"; Expression={"Web"}}, 
                @{Label="Item"; Expression={$_.Title}}, 
                @{Label="Url"; Expression={$_.ServerRelativeUrl}}, 
                @{Label="Parent"; Expression={$_.ParentWeb.ServerRelativeUrl}} | Write-Output
   
        if($StopAtWeb) {
            return;
        }
        Process-Webs -Webs $Webs
    } finally {
        Stop-SPAssignment $WebGc;
    }
}
 
Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction Inquire
 
$gc = Start-SPAssignment;
try{
    if(!$Site) {
        Write-Verbose "Site was neither given, nor in Pipe. Fetching Site from Url:$SiteCollectionUrl";
        if(!$SiteCollectionUrl) {
            Write-Error "Neither -Site, nor -SiteCollectionUrl was given.";
            Exit -1;
        }
        $Site = Get-SpSite -Identity $SiteCollectionUrl -AssignmentCollection $gc -ErrorAction SilentlyContinue;
        if ($Site -eq $null)
        {
            Write-Error "No SiteCollection with Identity '$SiteCollectionUrl' found. Exiting...";
            Exit -1;
        }
    }
    Process-Site -Site $Site;
} finally {
    Stop-SPAssignment $gc;
}

Freitag, 10. August 2018

Termstoremanagement mit gegebenen Term öffnen

Die Frage heute beim Kunden: Kann ich das "Term Store Management Tool" an einer bestimmten Stelle im TermStore öffnen - quasi den zu bearbeitenden Term gleich mitgeben? Die Antwort ist "ja" und findet sich in der Datei TermStoreManager.js.

Es kann mit dem query-Parameter termPath ein Pfad zu einem bestimmten Term übergeben werden. Dafür müssen alle IDs aller Eltern-Elemente (TermGroup, -Sets) hintereinander gestellt werden - mit "|"-Getrennt.

Das Ergebnis ist nicht schön, läuft aber. Aus einer beliebigen SP-Seite heraus also einfach den href setzen​:

function getManagementUrl(idPathsArray) {
    return _spPageContextInfo.webAbsoluteUrl + '/' + _spPageContextInfo.layoutsUrl + '/termstoremanager.aspx?termPath=' + idPathsArray.join('|')
}

Credentials in der PowerShell überprüfen lassen

Hintergrund:

Mitten in einem PowerShell-Skript beim Kunden steht "Get-Credentials" - wenn bei der Erfassung der credentials ein Fehler unterläuft, bricht das S​kript ab und man kann von vorne beginnen...

Die Anforderung:

Nach erfassen der Credentials sollen diese auf Gültigkeit überprüft werden.
Es gibt die eine oder andere Frage und auch eine Lösung in der TechNet-Gallery​...

Mir war das leider  nicht "generell" genug. Hier ist meine Lösung:

function Test-Credential { 
<#
    .SYNOPSIS
       checks some credentials.
    
      .PARAMETER Credential
        The Credentials to check
 
      .PARAMETER Retry
        retry on fail.
     
      .EXAMPLE
        Get-Credential dev\developer | Test-Credential -Verbose -Retry
         
#> 
    [OutputType([System.Management.Automation.PSCredential])] 
    [CmdletBinding()]
    Param ( 
        [Parameter( 
            Mandatory = $true, 
            ValueFromPipeLine = $true, 
            ValueFromPipelineByPropertyName = $true
        )] 
        [Alias( 'PSCredential' )] 
        [ValidateNotNull()] 
        [System.Management.Automation.PSCredential] 
        [System.Management.Automation.Credential()] 
        $Credential = [System.Management.Automation.PSCredential]::Empty,
        [switch]$Retry=$false
     ) 
    $processing = $true;
    while($processing) {
        
        $UserName = $Credential.username
        $networkCred = $Credential.GetNetworkCredential();
        $password = $networkCred.Password
        $domain = $networkCred.Domain
        Write-Verbose "Credentials supplied for user: $UserName"
        if(!$domain) {
            if($UserName -match "@") {
                # user was foo@bar.baz...
                Write-Verbose "User in UPN-Form. No domain-parsing will be done.."
            } else {
                # current domain
                $domain = "LDAP://" + ([ADSI]"").DistinguishedName
                Write-Verbose "No domain given. DistinguishedName of current domain: $domain"
            }
        } else {
            Write-Verbose "domain given as $domain in old NT-syntax."
            $test = Get-Command Get-ADDomain -ErrorAction SilentlyContinue
            if($test -eq $null) {
                $guess =  "$($UserName.Substring($domain.length+1))@$domain";
                Write-Verbose "CMDLet Get-ADDomain is not available. Guessing UPN-form for user as $guess";
                $UserName = $guess;
                $domain = "";
            } else {
                Write-Verbose "CMDLet Get-ADDomain is available. Fetching DistinguishedName..";
                $domain = Get-ADDomain $domain -ErrorAction Stop;
                $domain = "LDAP://" + $domain.DistinguishedName;
                Write-Verbose "DistinguishedName of domain is: $domain";
            }        
        }
        
 
        # now re-fetch the current domain using the supplied credentials.
        Write-Verbose "Validating credentials"
        $entry = New-Object System.DirectoryServices.DirectoryEntry($domain,$UserName,$Password)
 
        if ($entry.name -eq $null)
        {
            Write-Verbose "Authentication failed."
            $abort = $false;
            if($Retry) {
                $Credential = Get-Credential -UserName $user -Message "Failed. Please rety..." -ErrorAction SilentlyContinue
                if($Credential -eq $null) {
                    $abort = $true;
                }
            }
            if(!$Retry -or $abort) {
                throw "Validation failed..."
                return $null;
            }
        }
        else
        {
            Write-Verbose "Authentication succeeded."
            $processing = $false;
            return $Credential;
        } 
    }
}