Thomas Kramer

IT-COW | Oktober 2012

Importieren und Löschen von LDAP-Einträgen, Dokumenten und Feiertagen in Sharepoint mittels der Powershell

By Administrator at Oktober 06, 2012 18:16
Filed Under: Administration, Projekte, Studium

Für das Studium und dem Fach Skriptsprachen war eine Projektarbeit als Abschluss vorgesehen. Im Praktikum wurde Perl gelehrt aber für das Projekt stand die Wahl der Skriptsprache frei.

 

Da die Leute auf Xing mich bereits etliche Male wegen meiner Sharepoint-Kenntnisse angeschrieben haben und eine Personaldienstleisterin mir auch bestätigte dass diese Kenntnisse derzeit gefragt sind hatte ich beschlossen Sharepoint als Thema meiner Projektarbeit zu wählen.

 

Als weiteren Eckpunkt meiner Projektarbeit habe ich mich für die Windows Powershell entschieden. Die Powershell ist der Quasi-Nachfolger der alten Techniken VBScript und der DOS Eingabeaufforderung. Die neue Windows-Shell arbeitet objektorientiert und orientiert sich auch stark an Unix-Shells wie der Bash.

 

Erkennbar ist das z. B. daran dass sie Pipes und in if-Abfragen –eq für equals (entspricht) benutzt statt dem verbreiteten einfachen ==. Ein Zeilenabschlusszeichen benötigt sie übrigens nicht.

 

Ich wollte mich schon seit längerem darin einarbeiten und habe somit diese Gelegenheit genutzt, zudem ist sie auch ein zentraler Bestandteil der künftigen Windows Server-Versionen. Das Einarbeiten in diese Thematik war somit durchaus berufsrelevant.

 

Thematisch geht es in der Projektarbeit um folgende Punkte:

 

  • Importieren von LDAP-Personeneinträgen als Kontakte in Sharepoint
  • Importieren von Dokumenten in die Dokumentenbibliothek einer Sharepoint-Seite
  • Importieren von iCal-Kalendereinträgen in den Öffentlichen Kalender einer Sharepoint-Seite
  • Löschen aller importierten Daten durch ein separates Skript

 

Für die LDAP-Datei habe ich meinen bereits eingerichteten LDAP-Server benutzt, die vorhandenen Einträge exportiert und durch fiktive Einträge des Fake Name Generators ersetzt. Aus diesen Einträgen extrahiere ich die Personeneinträge, in dem Skript muss man dazu den Startpunkt (BaseDN-Eintrag) der LDAP-Hierarchie angeben ab der ausgelesen werden soll.

 

Als Dokumente war es zunächst naheliegend irgendwelche Bilder als Passfotos zu verwenden, aber ich wollte mich nicht mit dem Copyright beschäftigen. Daher habe ich einfach Bücher des Projekt Gutenberg-Projekts importiert, jeweils zu jeder Person eines mit dem Namen der Person als Dateinamen.

 

Außerdem importiert werden die Feiertage 2012 aus einer iCal-Kalenderdatei.

 

Und zu guter Letzt muss alles rückstandslos durch ein separates Skript wieder gelöscht werden können, aber nur die Daten die importiert wurden, keine händisch angelegten. Dazu versehe ich die Daten beim Importieren mit einer Signatur.

 

Für weitere Details siehe meine Projektarbeit:

 

Projektarbeit.pdf

 

Update 07.10.2012: Mittels des Befehls

 

$webclient.UploadFile((„http://$Server-IP:$Port/Dokumentenbibliothek/$fileName"), „DELETE“, "$fullPath")

 

wird ein Dokument in Sharepoint gelöscht, man beachte den zweiten Parameter "DELETE".

 

Dass ein Befehl der dem Namen zufolge Dokumente hochlädt auch welche löschen kann ist natürlich nicht wirklich gelungen, aber der Befehl wird ja auch als String übergeben und reicht ihn wahrscheinlich einfach nur weiter. Dennoch hätte ich an Microsofts Stelle den Befehlsnamen anders genannt oder die Möglichkeit zum Löschen unterbunden.

 

Jedenfalls, wenn man stattdessen

 

$webclient.UploadFile((„http://$Server-IP:$Port/Dokumentenbibliothek/"), „DELETE“, "$fullPath")

 

angibt wird sogar die gesamte Dokumentenbibliothek komplett und ohne weitere Rückfrage gelöscht, VORSICHT daher bei Anwendung dieses Befehls.

 

Update 29.10.2012: Der Powershell ist es übrigens egal wie man Variablen in Textausgaben einbindet:

 

1. "Es kostet " $cost " Euro."

2. "Es kostet " + $cost + " Euro."

3. "Es kostet $cost Euro."

 

Alles gültig - es müssen lediglich doppelte Anführungszeichen verwendet werden, folgendes:

 

4. 'Es kostet $cost Euro.'

 

schreibt dagegen den Variablennamen aus - demnach ist die Unterscheidung zwischen " und ' genauso wie bei der Bash unter Linux. Um es ein bisschen stringenter zu machen habe ich nun durchgängig auf Variante 3 gesetzt.

 

Außerdem setze ich jetzt auf eine GUID, um die durch ein Skript importierten Daten zu erkennen. Diese habe ich durch www.guidgen.com generieren lassen.

 

Zusätzlich habe ich auch noch einen Fehler im Delete-Skript beseitigt, dieses kam in eine Endlosschleife wenn neben den importierten Daten auch noch händisch angelegte vorhanden waren. Jetzt funktioniert es wie gewünscht, es ist mir aber eigentlich noch etwas zu langsam. Wenn man die Ausgaben in den Schleifen beseitigt müsste es deutlich schneller werden.

 

Übrigens ist mir bei der Powershell auch noch die fehlende Block-Sichtbarkeit der typenlosen Variablen aufgefallen. Wenn man z. B. in Java/.NET innerhalb einer Schleife eine neue Variable deklariert kann man auch nur innerhalb der Schleife darauf zugreifen - in Powershell-Skripten ist das anders, auf die Variablen dort kann man überall zugreifen. Im Prinzip kann man hier aber nicht mehr von Deklaration sprechen, weil ja kein Typ für die Variablen mehr angegeben wird.

 

Update 20.12.2012: Ich hatte Standard-Spalten in Sharepoint zweckentfremdet für die GUIDs (Datensatz-IDs), das habe ich nun anders gelöst. Dazu habe ich jeweils eine benutzerdefinierte Spalte in den Kalender und in die Kontakte hinzugefügt, namentlich “sciptGUID” – dort werden die GUIDs nun hineingeschrieben, die importierte von händisch angelegten Datensätzen unterscheiden.

 

Außerdem habe ich in der virtuellen Maschine in Sharepoint die Rechte für den Kalender und die Kontakte so eingeschränkt, dass normale Benutzer nur ihre selbst angelegten Kalender- und Kontaktdatensätze verändern (und speichern) können.

 

Das bedeutet dass normale Benutzer zwar die importierten Datensätze sehen, aber nicht verändern und speichern können. Versuchen sie es trotzdem erfolgt eine Fehlermeldung wegen unzureichender Rechte.

 

Es ist in Sharepoint zwar möglich Zeilenfilter/Datensatzfilter für die benutzerdefinierten Ansichten festzulegen, aber offenbar keine Spaltenfilter. Demnach ist es nicht möglich die Spalte scriptGUID ganz zu verbergen – das wäre insofern sinnvoll weil sie keinen Informationsgehalt für den Benutzer hat.

 

Leider kann ich die virtuelle Maschine hier natürlich nicht zur Verfügung stellen.

 

Nachfolgend die Quellcodes:

 

ImportDocuments.ps1:

 

##############################################################################################
## Skript v1.05 von Thomas Kramer vom 20.12.2012
##############################################################################################
## Zum Ausführen dieses Skripts muss zunächst die ExecutionPolicy auf Unrestricted
## gesetzt werden, siehe auch hier:
## http://technet.microsoft.com/en-us/library/ee176949.aspx
##############################################################################################

##############################################################################################
## Allgemeine Einstellungen
##############################################################################################
## Pfade für 1. Kontaktdateien (LDIF) und 2. Dokumente festlegen
$path = "./Desktop/Powershell/"
$docPath = "./Desktop/Powershell/Dokumente/"
## zugehörige Dateiendungen festlegen
$extension1 = "*.ldif"
$extension2 = ".txt"
## Basis, ab wo Personen in der LDAP-Datei zu finden sind:
$base = ",ou=thomas,ou=Adressbuch,dc=internal,dc=com"
## Konstante für Skript-Identifikation, generiert von http://www.guidgen.com/
$scriptGUID = "e8adebb6-5594-4583-beb0-5fbded4b45cd"
## URL für Dokumentenbibliothek
$documentPathURL = "http://winserver2003:100/Dokumentenbibliothek/"

##############################################################################################
## Einbinden der Sharepoint-Assemblies, um auf deren Objektmodell zugreifen zu können
##############################################################################################
[System.Reflection.Assembly]::LoadWithPartialName(„Microsoft.SharePoint“)

##############################################################################################
## Öffnet eine Webseitensammlung
##############################################################################################
$site = new-object -typename Microsoft.SharePoint.SPSite(„http://localhost:100/“)

##############################################################################################
## Erste Webseite einer Webseitensammlung öffnen
##
## Webseiten beziehen sich explizit auf die URLs, unter http://localhost könnte es bspw.
## eine weitere Seite http://localhost/Dokumente geben.
##
## OpenWeb-Methoden von Sharepoint siehe hier:
## http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spsite.openweb.aspx
## -> siehe auch http://blog.mastykarz.nl/inconvenient-opening-spsite-openweb/
##############################################################################################
## Aufrufen der Webseitenhierarchie einer Sharepoint-Seite: In der Webseite oben rechts auf
## Websiteaktionen/Einstellungen klicken und dann auf Websitehierarchie
##############################################################################################
$web = $site.OpenWeb()

##############################################################################################
## Öffnen der Kontakteseite von Sharepoint
##############################################################################################
$list = $web.lists["Kontakte"]

##############################################################################################
## Erstellung eines Webclients zum Uploaden von Dateien in die Sharepoint-Dokumentenbibliothek,
## sowie festlegen der nötigen Rechte
##############################################################################################
$webclient = new-object Net.WebClient
$webclient.Credentials = [System.Net.CredentialCache]::DefaultCredentials

##############################################################################################
## Importieren von Kontakten und Dokumenten
##############################################################################################
# Bildschirm löschen
clear
# Startzeit nehmen für Zeitmessung
$start = Get-Date
dir $path -filter $extension1 |
   ForEach-Object {
        # Neue Leerzeile
        [Environment]::NewLine  
        echo "##############################################################################################        "
        echo "## Verarbeitete Datei: $_"
        echo "##############################################################################################        "       
        [Environment]::NewLine          
        # auslesen der Daten in String-Array       
        $text = Get-Content $_.fullName
       
        # merkt sich ob ein neuer DN-Eintrag begonnen hat
        $found = $false
        # eingelesenen Text Zeile für Zeile durchgehen
        Foreach ($line in $text)
        {
          # neuen Eintrag gefunden, gekennzeichnet durch eine Leerzeile
          # expliziten Ende-Tag gibt es bei LDAP-Einträgen leider nicht
          if ($line.trim() -eq "")
          {
            if ($found)
            {
              # Daten als Kontakt importieren
              Write-Host "Person $firstname $lastname wird importiert" -foregroundcolor yellow -backgroundcolor red             
             
              $item = $list.items.add()
              $item[„Title“]       = $lastname
              $item[„FirstName“]   = $firstname
              $item[„Email“]       = $mail
              $item[„scriptGUID“]  = $scriptGUID
              $item.Update()

              # Zur Person zugehöriges Dokument importieren, falls vorhanden     
              dir $docPath -filter ($lastname + ", " + $firstname + $extension2) |
                ForEach-Object {              
                  Write-Host "-> Dokument für Person $firstname $lastname gefunden, wird importiert" -foregroundcolor yellow -backgroundcolor red                             
                  $webclient.UploadFile((„$documentPathURL“ + $_.Name), „PUT“, $_.FullName)                
                }             
                   
              $found = $false
            }
          }
       
          # Zeile enthält DN-Eintrag
          if ($line -match 'DN: ')
          {
            # aufsplitten was nach DN: kommt, das wieder in einen String
            # umwandeln und trimmen
            $temp1 = $line.split('DN: ')
            $temp2 = $temp1 | Out-String
            $temp2 = $temp2.trim()
           
            # Zeile enthält DN-Eintrag, der den Basis-Eintrag enthält
            if ($temp2 -match $base.trim().toLower())
            {
              $found = $true
             
              # das voranstehende cn= wegkürzen
              $temp3 = $temp2.substring(3)
              # nachfolgende Base-Zeile wegkürzen
              $temp3 = $temp3.substring(0,$temp3.length-$base.length)
              # aufsplitten in Vor- und Nachnamen
              [StringSplitOptions]$option="None"
              $name  = $temp3.split('\,',$option)
              $firstname = $name[2]
              $firstname = $firstname.trim()
              $lastname = $name[0]
              $lastname = $lastname.trim()
            }               
          }
         
          # wenn im gleichen DN-Eintrag eine Mail-Adresse gefunden wird, zwischenspeichern
          if ($found)
          {
            if ($line -match 'mail: ')
            {
              $mail = $line.substring(6).trim() 
            }
          }              
        } # Ende der Foreach-Schleife
   }

# letzter Eintrag in Datei darf nicht vergessen werden, wenn keine Leerzeile mehr folgte   
if ($found)
{
   Write-Host "Person $firstname $lastname wird importiert" -foregroundcolor yellow -backgroundcolor red             
  
   # Daten als Kontakte importieren
   $item = $list.items.add()
   $item[„Title“]       = $lastname
   $item[„FirstName“]   = $firstname
   $item[„Email“]       = $mail
   $item[„scriptGUID“]  = $scriptGUID
   $item.Update()
  
   # Zur Person zugehöriges Dokument importieren, falls vorhanden     
   dir $docPath -filter ($lastname + ", " + $firstname + $extension2) |
     ForEach-Object {              
       Write-Host "-> Dokument für Person $firstname $lastname gefunden, wird importiert" -foregroundcolor yellow -backgroundcolor red                             
       $webclient.UploadFile((„$documentPathURL“ + $_.Name), „PUT“, $_.FullName)                
     }       
                   
   $found = $false
}

##############################################################################################
## Öffnen des öffentlichen Kalenders
##############################################################################################
$list = $web.lists["Öffentlicher Kalender"]

##############################################################################################
## Importieren von Feiertagen
############################################################################################## 
## Dateiendung festlegen
$extension1 = "*.ics"

# alle Dateien mit dieser Endung durchgehen
dir $path -filter $extension1 |
   ForEach-Object {
        # Neue Leerzeile
        [Environment]::NewLine  
        echo "##############################################################################################        "
        echo "## Verarbeitete Datei: $_"
        echo "##############################################################################################        "       
        [Environment]::NewLine 
        # auslesen der Daten in String-Array
        $text = Get-Content $_.fullName
       
        # merkt sich ob ein neuer Feiertag-Eintrag begonnen hat
        $found = $false
        # eingelesenen Text Zeile für Zeile durchgehen       
        Foreach ($line in $text)
        {
          # Ende des Eintrags gefunden, importieren
          if ($line.trim() -eq "END:VEVENT")
          {
            if ($found)
            {
              # Daten als Feiertag in den Kalender importieren
              Write-Host "Feiertag $summary von $dtstart bis $dtend wird importiert" -foregroundcolor yellow -backgroundcolor red             
             
              $item = $list.items.add()
              $item[„Title“]        = $summary
              $item[„EventDate“]    = $dtstart
              $item[„EndDate“]      = $dtend
              $item[„scriptGUID“]   = $scriptGUID
              $item.Update()            
             
              $found = $false
            }
          }  
         
          # neuen Feiertag gefunden
          if ($line -match 'BEGIN:VEVENT')
          {
            # wenn BEGIN:VEVENT gefunden wurde, sind auch immer die Zeilen
            # SUMMARY, DTSTART und DTEND vorhanden, weitere Überprüfung nicht
            # notwendig                       
           
            $found = $true
          }         
                   
          # Zeile enthält Summary-Eintrag
          if ($line -match 'SUMMARY:')
          {
             # aufsplitten
             [StringSplitOptions]$option="None"
             $temp1   = $line.split(':',$option)
             $summary = $temp1[1]            
             $summary = $summary.trim()                       
          }         
         
          # Zeile enthält dtstart-Eintrag
          if ($line -match 'DTSTART;VALUE=DATE:')
          {
             # aufsplitten
             [StringSplitOptions]$option="None"
             $temp1   = $line.split(':',$option)
             $dtstart = $temp1[1]            
             $dtstart = $dtstart.trim()  
            
             $year    = $dtstart.substring(0,4)
             $month   = $dtstart.substring(4,2)
             $day     = $dtstart.substring(6,2)
            
             $dtstart = "$month-$day-$year 00:00:00"
          }                   
         
          # Zeile enthält dtend-Eintrag
          if ($line -match 'DTEND;VALUE=DATE:')
          {
             # aufsplitten
             [StringSplitOptions]$option="None"
             $temp1   = $line.split(':',$option)
             $dtend   = $temp1[1]            
             $dtend   = $dtend.trim()                       
            
             $year    = $dtend.substring(0,4)
             $month   = $dtend.substring(4,2)
             $day     = $dtend.substring(6,2)
            
             # Endtag ist immer einen Tag später in den iCal-Einträgen,
             # richtig ist als Ende aber der gleiche Tag
             # um 23:59:59
            
             [datetime]$tempDate = [datetime] "$month-$day-$year 23:59:59"
             $tempDate = $tempDate.addDays(-1)
            
             $year    = $tempDate.Year 
             $month   = $tempDate.Month
             $day     = $tempDate.Day                                     
            
             $dtend = "$month-$day-$year 23:59:59"            
          }                                       
        } # Ende der Foreach-Schleife                                       
   }      

# Endzeit nehmen und benötigte Zeit berechnen
$stop = Get-Date
$neededTime = ($stop - $start).TotalSeconds
# neue Leerzeile und Ausgabe der benötigten Zeit
[Environment]::NewLine
Write-Host "Importieren nach $neededTime Sekunden erledigt" -foregroundcolor blue -backgroundcolor white             

# Nach den Referenzen nach denen ich geschaut habe wird eine geöffnete Webseite nicht explizit wieder geschlossen,
# siehe auch http://msdn.microsoft.com/en-us/library/ms473155.aspx

 

DeleteDocuments.ps1:

 

##############################################################################################
## Skript v1.05 von Thomas Kramer vom 20.12.2012
##############################################################################################
## Zum Ausführen dieses Skripts muss zunächst die ExecutionPolicy auf Unrestricted
## gesetzt werden, siehe auch hier:
## http://technet.microsoft.com/en-us/library/ee176949.aspx
##############################################################################################

##############################################################################################
## Allgemeine Einstellungen
##############################################################################################
## absoluten Pfad der Dokumente festlegen, das Löschen funktioniert nicht mit relativen Pfaden!
$docPath = "C:\Dokumente und Einstellungen\Administrator\Desktop\Powershell\Dokumente\"
## Dateiendung der Dokumente festlegen
$extension = ".txt"
## Konstante für Skript-Identifikation, generiert von http://www.guidgen.com/
$scriptGUID = "e8adebb6-5594-4583-beb0-5fbded4b45cd"
## URL für Dokumentenbibliothek
$documentPathURL = "http://winserver2003:100/Dokumentenbibliothek/"

##############################################################################################
## Einbinden der Sharepoint-Assemblies, um auf deren Objektmodell zugreifen zu können
##############################################################################################
[System.Reflection.Assembly]::LoadWithPartialName(„Microsoft.SharePoint“)

##############################################################################################
## Öffnet eine Webseitensammlung
##############################################################################################
$site = new-object -typename Microsoft.SharePoint.SPSite(„http://localhost:100/Dokumentenbibliothek“)

##############################################################################################
## Erste Webseite einer Webseitensammlung öffnen
##
## Webseiten beziehen sich explizit auf die URLs, unter http://localhost könnte es bspw.
## eine weitere Seite http://localhost/Dokumente geben.
##
## OpenWeb-Methoden von Sharepoint siehe hier:
## http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spsite.openweb.aspx
## -> siehe auch http://blog.mastykarz.nl/inconvenient-opening-spsite-openweb/
##############################################################################################
## Aufrufen der Webseitenhierarchie einer Sharepoint-Seite: In der Webseite oben rechts
## auf Websiteaktionen/Einstellungen klicken und dann auf Websitehierarchie
##############################################################################################
$web = $site.OpenWeb()

##############################################################################################
## Öffnen der Kontakteseite von Sharepoint
##############################################################################################
$list = $web.lists[„Kontakte“]

##############################################################################################
## Erstellung eines Webclients zum Löschen von Dateien in die Sharepoint-Dokumentenbibliothek,
## sowie festlegen der nötigen Rechte
##############################################################################################
$webclient = new-object Net.WebClient
$webclient.Credentials = [System.Net.CredentialCache]::DefaultCredentials

##############################################################################################
## Löschen von Kontakten und Dokumenten
##############################################################################################
# Bildschirm löschen
clear
echo "##############################################################################################        "
echo "## Lösche importierte Kontakte und Dokumente"
echo "##############################################################################################        "       
[Environment]::NewLine    
# Startzeit nehmen für Zeitmessung
$start = Get-Date

# solange Ausführen wie noch durch das Skript importierte Daten vorhanden sind
$counter = 0
$count = $list.items.count
while ($counter -ne $count)
{
  $item = $list.items[$list.items.count-1]
  # Nur importierte Kontakte berücksichtigen, keine von Hand angelegten
  if ($item[„scriptGUID“] -eq $scriptGUID)
  {
    # Löschen des Kontaktes
    $firstName = $item["FirstName"]
    $lastName = $item["Title"]
    Write-Host "Person $firstName $lastName wird gelöscht" -foregroundcolor yellow -backgroundcolor red               
    $item.Delete()
   
    # Löschen des zugehörigen Dokumentes
    Write-Host "-> Dokument für Person $firstName $lastName wird gelöscht, falls vorhanden." -foregroundcolor yellow -backgroundcolor red                             
    $fileName = $lastName + ", " + $firstName + $extension
    $fullPath = $docPath + $fileName
   
    # mit dem nachfolgenden Befehl wird tatsächlich ein Dokument in der Dokumentenbibliothek gelöscht!
    # man beachte den 2. Parameter "DELETE"!
    # die Originaldatei muss dazu noch auf der Festplatte vorhanden sein, um sie in Sharepoint löschen zu können.
    # (Das Importieren sollte bereits ein paar Sekunden her sein, sonst könnte eine Datei stehen bleiben)
    $webclient.UploadFile((„$documentPathURL$fileName"), „DELETE“, "$fullPath")                       
  }
  $counter += 1
}

##############################################################################################
## Öffnen des öffentlichen Kalenders
##############################################################################################
$list = $web.lists["Öffentlicher Kalender"]

##############################################################################################
## Löschen der Feiertage
##############################################################################################
[Environment]::NewLine    
echo "##############################################################################################        "
echo "## Lösche importierte Feiertage "
echo "##############################################################################################        "       
[Environment]::NewLine    

# solange Ausführen wie noch durch das Skript importierte Daten vorhanden sind
$counter = 0
$count = $list.items.count
while ($counter -ne $count)
{
  $item = $list.items[$list.items.count-1]
  # Nur importierte Feiertage berücksichtigen, keine von Hand angelegten
  if ($item[„scriptGUID“] -eq $scriptGUID)
  {
    # Löschen des Feiertages
    $title = $item["Title"]
    Write-Host "Feiertag $title wird gelöscht" -foregroundcolor yellow -backgroundcolor red               
    $item.Delete() 
  }
  $counter += 1 
}

# Endzeit nehmen und benötigte Zeit berechnen
$stop = Get-Date
$neededTime = ($stop - $start).TotalSeconds
# neue Leerzeile und Ausgabe der benötigten Zeit
[Environment]::NewLine
Write-Host "Löschen nach $neededTime Sekunden erledigt" -foregroundcolor blue -backgroundcolor white             

# Nach den Referenzen nach denen ich geschaut habe wird eine geöffnete Webseite nicht explizit wieder geschlossen,
# siehe auch http://msdn.microsoft.com/en-us/library/ms473155.aspx

 

Monats-Liste