diff --git a/src/SharepointFileSystem/0Console/.gitignore b/src/SharepointFileSystem/0Console/.gitignore new file mode 100644 index 0000000..154e127 --- /dev/null +++ b/src/SharepointFileSystem/0Console/.gitignore @@ -0,0 +1,477 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/src/SharepointFileSystem/0Console/AppCreationScripts/Cleanup.ps1 b/src/SharepointFileSystem/0Console/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..38bcce4 --- /dev/null +++ b/src/SharepointFileSystem/0Console/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,66 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='MS Graph application name, must be URL safe characters, e.g. no spaces and & etc.')] + [string] $appName, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId +) + +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = 'Stop' + +Function Cleanup +{ +<# +.Description +This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script +#> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'client' ($appName) if needed" + + $apps=Get-AzureADApplication -Filter "DisplayName eq '$appName'" + + foreach ($app in $apps) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed." + } + +} + +Cleanup -Credential $Credential -tenantId $TenantId diff --git a/src/SharepointFileSystem/0Console/AppCreationScripts/Configure.ps1 b/src/SharepointFileSystem/0Console/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..2891583 --- /dev/null +++ b/src/SharepointFileSystem/0Console/AppCreationScripts/Configure.ps1 @@ -0,0 +1,213 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='MS Graph application display name')] + [string] $displayName, + [Parameter(Mandatory=$False, HelpMessage='MS Graph application name, must be URL safe characters, e.g. no spaces and & etc.')] + [string] $appName, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId +) + +<# + This script creates the Azure AD applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Azure AD applications. + + Before running this script you need to install the AzureAD cmdlets as an administrator. + For this: + 1) Run Powershell as an administrator + 2) in the PowerShell window, type: Install-Module AzureAD + + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Create a password that can be used as an application key +Function ComputePassword +{ + $aesManaged = New-Object "System.Security.Cryptography.AesManaged" + $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC + $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros + $aesManaged.BlockSize = 128 + $aesManaged.KeySize = 256 + $aesManaged.GenerateKey() + return [System.Convert]::ToBase64String($aesManaged.Key) +} + +# Create an application key +# See https://www.sabin.io/blog/adding-an-azure-active-directory-application-and-key-using-powershell/ +Function CreateAppKey([DateTime] $fromDate, [double] $durationInYears, [string]$pw) +{ + $endDate = $fromDate.AddYears($durationInYears) + $keyId = (New-Guid).ToString(); + $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential + $key.StartDate = $fromDate + $key.EndDate = $endDate + $key.Value = $pw + $key.KeyId = $keyId + return $key +} + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess.Add($resourceAccess) + } + } + } +} + +# +# Exemple: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +Function ConfigureApplications +{ +<#.Description + This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters +#> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling you to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the client AAD application + Write-Host "Creating the AAD application ($appName)" + # Get a 2 years application key for the client Application + $pw = ComputePassword + $fromDate = [DateTime]::Now; + $key = CreateAppKey -fromDate $fromDate -durationInYears 2 -pw $pw + $clientAppKey = $pw + $clientAadApplication = New-AzureADApplication -DisplayName "$displayName" ` + -ReplyUrls "https://$appName" ` + -IdentifierUris "https://$tenantName/$appName" ` + -PasswordCredentials $key ` + -PublicClient $False + + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + Write-Host "Done creating the client application ($appName)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredApplicationPermissions "User.Read.All"; + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $certificateDescriptor = @{ }; + $dictionary = [ordered]@{ "Instance" = "https://login.microsoftonline.com/{0}"; "ApiUrl" = "https://graph.microsoft.com/";"Tenant" = $tenantName;"ClientId" = $clientAadApplication.AppId;"ClientSecret" = $clientAppKey;"CertificateName" = "CN=DaemonConsoleCert"; "Certificate" = $certificateDescriptor }; + $dictionary | ConvertTo-Json | Out-File $configFile + Write-Host "" + Write-Host "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal": + Write-Host "- For 'client'" + Write-Host " - Navigate to '$clientPortalUrl'" + Write-Host " - Navigate to the API permissions page and click on 'Grant admin consent for {tenant}'" + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppId$appName
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/src/SharepointFileSystem/0Console/AppCreationScripts/RegisterApp.ps1 b/src/SharepointFileSystem/0Console/AppCreationScripts/RegisterApp.ps1 new file mode 100644 index 0000000..e77aae3 --- /dev/null +++ b/src/SharepointFileSystem/0Console/AppCreationScripts/RegisterApp.ps1 @@ -0,0 +1,13 @@ +##Switch-AzureMode AzureResourceManager +##Connect-AzAccount +##Update-Module -Name Az +##az login +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force +Install-Module AzureAD +$tenantId = "yourTenantIdGuid" +$appname = "daemon-console" +$displayname = "MSGraph Daemon Console Test App" +## if running the registration a 2nd time, uncomment this line +#. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId -AppName $appname -DisplayName $displayname + diff --git a/src/SharepointFileSystem/0Console/AppCreationScripts/createdApps.html b/src/SharepointFileSystem/0Console/AppCreationScripts/createdApps.html new file mode 100644 index 0000000..da9c8a5 --- /dev/null +++ b/src/SharepointFileSystem/0Console/AppCreationScripts/createdApps.html @@ -0,0 +1,4 @@ + + + +
ApplicationAppIdUrl in the Azure portal
client0aac9e6f-f2ab-40c4-8332-ef588854d90ddaemon-console
diff --git a/src/SharepointFileSystem/0Console/Experiments/MSGraphExamples.cs b/src/SharepointFileSystem/0Console/Experiments/MSGraphExamples.cs new file mode 100644 index 0000000..7308b3f --- /dev/null +++ b/src/SharepointFileSystem/0Console/Experiments/MSGraphExamples.cs @@ -0,0 +1,142 @@ +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.Graph.Models; + +namespace dotnet_console_microsoft_graph.Experiments; + +internal static class MSGraphExamples { + public static async Task Main(GraphServiceClient betaGraphClient) { + //// Other TokenCredentials examples are available at https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/tokencredentials.md + //var scopes = new[] { "User.Read", "Mail.Read", "User.ReadBasic.All" }; + //var interactiveBrowserCredentialOptions = new InteractiveBrowserCredentialOptions { + // ClientId = "CLIENT_ID" + //}; + //var tokenCredential = new InteractiveBrowserCredential(interactiveBrowserCredentialOptions); + + // GraphServiceClient constructor accepts tokenCredential + //var v1GraphClient = new GraphServiceClient(tokenCredential, scopes);// client for the v1.0 endpoint + //var betaGraphClient = new Microsoft.Graph.GraphServiceClient(tokenCredential, scopes);// client for the beta endpoint + + // Perform batch request using the beta client + await PerformRequestWithHeaderAndQueryRequestAsync(betaGraphClient); + + // Perform batch request using the beta client + await PerformCustomRequestWithHeaderAndQueryAsync(betaGraphClient); + + // Perform batch request using the v1 client + await PerformBatchRequestAsync(betaGraphClient); + + // Perform paged request using the v1 client + await IteratePagedDataAsync(betaGraphClient); + } + public static async Task ShowTenantUsersAsync(GraphServiceClient graphClient) { + try { + // Get the requestInformation to make a GET request + var requestInformation = graphClient + .DirectoryObjects + .ToGetRequestInformation(); + Console.WriteLine("requestInformation.URI=" + requestInformation.URI); + + // get all users on tenant + var users = await graphClient.Users.GetAsync( + requestConfiguration => requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName", "mail" }); + if (users != null && users.Value != null) { + foreach (var user in users.Value) { + if (user == null) continue; + Console.WriteLine($"User({user.Id}):Name:{user.DisplayName}:{user.Mail}"); + } + } + } + catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) { + Console.WriteLine($"Error({ex?.Error?.Code}):{ex?.Error?.Message}"); + } + catch (AuthenticationFailedException ex) { + Console.WriteLine(ex.Message); + } + catch (Exception ex) { + Console.WriteLine(ex.Message); + } + } + + private static async Task PerformBatchRequestAsync(GraphServiceClient graphClient) { + Console.WriteLine("-----------Performing batch requests-----------"); + var userRequest = graphClient.Me.ToGetRequestInformation();// create request object to get user information + var messagesRequest = graphClient.Me.Messages.ToGetRequestInformation();// create request object to get user messages + + // Build the batch + var batchRequestContent = new BatchRequestContent(graphClient); + var userRequestId = await batchRequestContent.AddBatchRequestStepAsync(userRequest); + var messagesRequestId = await batchRequestContent.AddBatchRequestStepAsync(messagesRequest); + + // Send the batch + var batchResponse = await graphClient.Batch.PostAsync(batchRequestContent); + + // Get the user info + var user = await batchResponse.GetResponseByIdAsync(userRequestId); + Console.WriteLine($"Fetched user with name {user.DisplayName} via batch"); + + // Get the messages data + var messagesResponse = await batchResponse.GetResponseByIdAsync(messagesRequestId); + List messages = messagesResponse.Value; + Console.WriteLine($"Fetched {messages.Count} messages via batch"); + Console.WriteLine("-----------Done with batch requests-----------"); + } + + private static async Task IteratePagedDataAsync(GraphServiceClient graphClient) { + Console.WriteLine("-----------Performing paged requests-----------"); + var firstPage = await graphClient.Me.Messages.GetAsync();// fetch first paged of messages + + var messagesCollected = new List(); + // Build the pageIterator + var pageIterator = PageIterator.CreatePageIterator( + graphClient, + firstPage, + message => { + messagesCollected.Add(message); + return true; + },// per item callback + request => { + Console.WriteLine($"Requesting new page with url {request.URI.OriginalString}"); + return request; + }// per request/page callback to reconfigure the request + ); + + // iterated + await pageIterator.IterateAsync(); + + // Get the messages data; + Console.WriteLine($"Fetched {messagesCollected.Count} messages via page iterator"); + Console.WriteLine("-----------Done with paged requests-----------"); + } + + private static async Task PerformRequestWithHeaderAndQueryRequestAsync(Microsoft.Graph.GraphServiceClient graphClient) { + Console.WriteLine("-----------Performing configured requests-----------"); + + var userResponse = await graphClient.Users.GetAsync(requestConfiguration => { + requestConfiguration.QueryParameters.Select = new[] { "id", "displayName" };// set select + requestConfiguration.QueryParameters.Filter = "startswith(displayName, 'al')";// set filter for users displayName starting with 'al' + requestConfiguration.QueryParameters.Count = true; + requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");//set the header + }); + + Console.WriteLine($"Fetched {userResponse.Value.Count} users with displayName starting with 'al'"); + Console.WriteLine("-----------Done with configured requests-----------"); + } + + private static async Task PerformCustomRequestWithHeaderAndQueryAsync(Microsoft.Graph.GraphServiceClient graphClient) { + Console.WriteLine("-----------Performing customized request-----------"); + + var requestInformation = graphClient.Users.ToGetRequestInformation(requestConfiguration => { + requestConfiguration.QueryParameters.Select = new[] { "id", "displayName" };// set select + requestConfiguration.QueryParameters.Filter = "startswith(displayName, 'al')";// set filter for users displayName starting with 'al' + requestConfiguration.QueryParameters.Count = true; + requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");//set the header + }); + + var userResponse = await graphClient.RequestAdapter.SendAsync( + requestInformation, Microsoft.Graph.Models.UserCollectionResponse.CreateFromDiscriminatorValue); + + Console.WriteLine($"Fetched {userResponse.Value.Count} users with displayName starting with 'al'"); + Console.WriteLine("-----------Done with customized requests-----------"); + } +} \ No newline at end of file diff --git a/src/SharepointFileSystem/0Console/Experiments/SharepointExamples.cs b/src/SharepointFileSystem/0Console/Experiments/SharepointExamples.cs new file mode 100644 index 0000000..7b91263 --- /dev/null +++ b/src/SharepointFileSystem/0Console/Experiments/SharepointExamples.cs @@ -0,0 +1,132 @@ +using Azure.Identity; +using Microsoft.Graph; + +namespace dotnet_console_microsoft_graph.Experiments; + +internal static class SharepointExamples { + public static async Task Main(GraphServiceClient betaGraphClient) { + await GetAllSharepointSitesAsync(betaGraphClient); + } + + public static async Task GetAllSharepointSitesAsync(GraphServiceClient graphClient) { + try { + //get sharepoint sites + + // get all sites + var sites = await graphClient.Sites.GetAsync(); + //requestConfiguration => requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName", "mail" }); + if (sites != null && sites.Value != null) { + foreach (var site in sites.Value) { + if (site == null) continue; + Console.WriteLine($"site({site.Id}):Name:{site.Name}:{site.DisplayName}"); + } + if (sites.Value.Count == 0) { Console.WriteLine("no sites found"); } + } + else { Console.WriteLine("no sites found"); } + } + catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) { + Console.WriteLine($"Error({ex?.Error?.Code}):{ex?.Error?.Message}"); + } + catch (AuthenticationFailedException ex) { + Console.WriteLine(ex.Message); + } + catch (Exception ex) { + Console.WriteLine(ex.Message); + } + + } + public static async Task GetSharepointSiteAsync(GraphServiceClient graphClient, string siteid, string driveid) { + try { + var site = await graphClient + .Sites[$"{siteid}"] + .GetAsync(requestConfiguration => { + //requestConfiguration.QueryParameters.Select = new string[] { "id", "createdDateTime", "displayName" }; + requestConfiguration.QueryParameters.Expand = new string[] { "drives", "lists" }; + }); + if (site != null) { + Console.WriteLine($"site({site.Id}):Name:{site.Name}:{site.DisplayName}"); + + //site.Drives.get + if (site.Drives != null) { + foreach (var drive in site.Drives) { + if (drive == null) continue; + Console.WriteLine($" drive({drive.Id}):Name:{drive.Name}:ParentReference({drive.ParentReference})"); + } + var _d = await graphClient + .Drives[driveid] + .GetAsync(); + //.GetAsync(requestConfiguration => { + //requestConfiguration.QueryParameters.Expand = new string[] { "items" };});//throws oData error + //requestConfiguration.QueryParameters.Expand = new string[] { "children" };});//throws oData error + if (_d != null) { + + // display all drive.Items + if (_d.Items != null) { + foreach (var item in _d.Items) { + if (item == null) continue; + Console.WriteLine($" Item({item.Id}):Name:{item.Name}:OdataType({item.OdataType}):folderChildCount:{item.Folder?.ChildCount ?? 0}"); + } + } + else { Console.WriteLine($" no drive({_d.Name}) items found"); } + + // display all drive.List.Items + if (_d.List?.Items != null) { + foreach (var item in _d.List.Items) { + if (item == null) continue; + Console.WriteLine($" ListItem({item.Id}):Name:{item.Name}:OdataType({item.OdataType})"); + } + } + else { Console.WriteLine($" no drive({_d.Name}) Listitems found"); } + } + else { Console.WriteLine(" no Drive() found"); } + } + else { Console.WriteLine(" no Drives found"); } + + + + if (site.Lists != null) { + foreach (var list in site.Lists) { + if (list == null) continue; + Console.WriteLine($" list({list.Id}):Name:{list.Name}:ParentReference({list.ParentReference})"); + if (list.Items != null) { + foreach (var item in list.Items) { + if (item == null) continue; + Console.WriteLine($" item({item.Id}):Name:{item.Name}:ParentReference({item.ParentReference})"); + } + } + } + } + else { Console.WriteLine(" no Lists found"); } + + if (site.Items != null) { + foreach (var item in site.Items) { + if (item == null) continue; + Console.WriteLine($" item({item.Id}):Name:{item.Name}:OdataType({item.OdataType})"); + + } + } + else { Console.WriteLine("no Items found"); } + } + else { Console.WriteLine("no sites found"); } + } + catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) { + Console.WriteLine($"Error({ex?.Error?.Code}):{ex?.Error?.Message}"); + } + catch (AuthenticationFailedException ex) { + Console.WriteLine(ex.Message); + } + catch (Exception ex) { + Console.WriteLine(ex.Message); + } + } + public static async Task CreateNewSubDirectoryAsync(GraphServiceClient graphClient, string siteid, string driveid) { + //v4 Get the requestInformation to make a POST request + //var directoryObject = new DirectoryObject() { + // Id = Guid.NewGuid().ToString() + //var requestInformation = graphServiceClient + // .DirectoryObjects + // .ToPostRequestInformation(directoryObject); + //TODO v5 a new Sub Directory + } +} + diff --git a/src/SharepointFileSystem/0Console/Program.cs b/src/SharepointFileSystem/0Console/Program.cs new file mode 100644 index 0000000..1b81593 --- /dev/null +++ b/src/SharepointFileSystem/0Console/Program.cs @@ -0,0 +1,81 @@ +using dotnet_console_microsoft_graph.Experiments; +using Microsoft.Extensions.Configuration; +using Microsoft.Graph.Models; +using MSGraphAuth; + +/// This sample shows how to query the Microsoft Graph from a daemon application +/// which uses application permissions. +/// +/// The extended project goal is to provide a Sharepoint System.IO.File and Folder abstraction for integration services +/// + +/* + Application permissions, +follow the instructions in .\readme.md + +[X] connect to graph as daemon +[X] list AAD users +[X] list sharepoint sites +[X] list drive(s) +[] list folders +[] list folder subfolders +[] list folder documents +[] perform CRUD on folder +[] perform CRUD on document + */ + +var config = new ConfigurationBuilder() + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) + .AddJsonFile("appsettings.json") + .AddUserSecrets() + .Build(); + +//connect to sharepoint +var ClientAppName = config["ClientAppName"]; +var ClientAppShortName = config["ClientAppShortName"]; +var Instance = config["Instance"]; +var ApiUrl = config["ApiUrl"]; +var Tenant = config["Tenant"]; +var TenantId = config["TenantId"]; +var ClientId = config["ClientId"]; +var ClientSecret = config["ClientSecret"]; +var sharepointSiteId = config["SharepointSiteId"]; +var sharepointDriveId = config["SharepointDriveId"]; + +//scopes are not required as this is a deamon app, and they are specified in AAD, they are listed here as a reminder to set them using the Azure Portal +var scopes = new[] {"offline_access" + ,"SharePointTenantSettings.ReadWrite.All" + ,"Directory.ReadWrite.All" + ,"Sites.Read.All" + ,"Files.ReadWrite.All" + ,"User.Read.All" + ,"BrowserSiteLists.ReadWrite.All" + , "openid", "profile", "User.Read" }; + +var client = new OAuth2ClientCredentialsGrantService( + ClientId, ClientSecret, Instance, Tenant, TenantId, ApiUrl + , null); +var graphClient = client.GetClientSecretClient(); +await MSGraphExamples.ShowTenantUsersAsync(graphClient); +await SharepointExamples.GetAllSharepointSitesAsync(graphClient); +await SharepointExamples.GetSharepointSiteAsync(graphClient, sharepointSiteId, sharepointDriveId); + +//Next steps +//connect to SPDocuments folder +//create folder topfldr +//topfldr exists? - expect: success +//create child folder topfolder/childfolder +//repeat create child folder topfolder/childfolder - expect success +//topfolder/childfolder exists? - expect: success +//upload new document topdoc.docx to topfldr (hint: clone TmpDoc.docx to topdoc.docx) +//upload new document topdoc.docx to topfldr - expect success +//topfolder/topdoc.docx exists? +//upload new document childdoc.docx to topfolder/childfolder +//topfolder/childfolder/childoc.docx exists? +//missingFolder exists? expect: - expect exception +//missingFolder/missingFolder does not exist - expect exception +//topfolder/missingFolder does not exist - expecte exception +//topfolder/missing.docx does not exist - expecte exception +//topfolder/missingFolder/missing.docx does not exist - expecte exception +//download topfolder/childfolder/childoc.docx to local filesystem + diff --git a/src/SharepointFileSystem/0Console/README.md b/src/SharepointFileSystem/0Console/README.md new file mode 100644 index 0000000..82a868f --- /dev/null +++ b/src/SharepointFileSystem/0Console/README.md @@ -0,0 +1,50 @@ +# .NET v7.0 console app connected to Microsoft Graph + +This a project based on the MSGraph Console App template that connects to Microsoft Graph. + + +## Minimal Path to Awesome 🚀 + +Follow the instructions to successfully run your Console app with Microsoft Graph. + +### 1. Register an Azure Active Directory app + +Every app that uses Azure AD for authentication must be registered with Azure AD. You can register app through Azure Portal or by using Azure CLI. Please follow one of the options to register your app: + +
+ Register an app by using Azure CLI on the Azure Portal +* run AppCreationScripts, it will update appsettings.json +* Open Powershell as admin, open RegisterApp.ps1 +* copy into new ps file +* change the values as per your Azure +* run the script +* do not commit the secret values into git, move sensitive data out of appsettings.json +* cut the values from appsettings.json into user secrets +* ensure there are only blank values in appsettings.json +* Open Azure Portal AAD|Enterprise Applications blade: https://portal.azure.com/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AppAppsPreview/menuId~/null +* open your app +* open permissions +* to request additional permissions for this application, use the 'application registration​' +* grant the following scopes: + - SharePointTenantSettings.ReadWrite.All + - Directory.ReadWrite.All + - Sites.Read.All + - Files.ReadWrite.All + - User.Read.All + - BrowserSiteLists.ReadWrite.All +* go back to your app ( back button on history) +* Grant admin Consnet for {Tenant name} +
+ +### 2. Run your Console app + +* run the application +* create a sharepoint site, list, subfolder, add a document +* 2nd time: you can choose a SiteId and DriveId to 'work on' +* add SharepointSiteId and SharepointDriveId to UserSecrets too. + +## Reference + +* [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) +* [MSGraph v5 upgrade notes, so v4 examples will also work](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/feature/5.0/docs/upgrade-to-v5.md) +* []() diff --git a/src/SharepointFileSystem/0Console/TmpDoc.docx b/src/SharepointFileSystem/0Console/TmpDoc.docx new file mode 100644 index 0000000..025c3d4 Binary files /dev/null and b/src/SharepointFileSystem/0Console/TmpDoc.docx differ diff --git a/src/SharepointFileSystem/0Console/appsettings.json b/src/SharepointFileSystem/0Console/appsettings.json new file mode 100644 index 0000000..f6885aa Binary files /dev/null and b/src/SharepointFileSystem/0Console/appsettings.json differ diff --git a/src/SharepointFileSystem/0Console/dotnet-console-microsoft-graph.csproj b/src/SharepointFileSystem/0Console/dotnet-console-microsoft-graph.csproj new file mode 100644 index 0000000..584f499 --- /dev/null +++ b/src/SharepointFileSystem/0Console/dotnet-console-microsoft-graph.csproj @@ -0,0 +1,31 @@ + + + + Exe + net7.0 + dotnet_console_microsoft_graph + enable + enable + e62d6b07-7f9a-4cec-b3e7-416cc042700d + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/SharepointFileSystem/0Console/firsttimeuse.png b/src/SharepointFileSystem/0Console/firsttimeuse.png new file mode 100644 index 0000000..a4d5a8d Binary files /dev/null and b/src/SharepointFileSystem/0Console/firsttimeuse.png differ diff --git a/src/SharepointFileSystem/FileSystemAbstraction/Exceptions/FileNotFoundException.cs b/src/SharepointFileSystem/FileSystemAbstraction/Exceptions/FileNotFoundException.cs new file mode 100644 index 0000000..5e94e41 --- /dev/null +++ b/src/SharepointFileSystem/FileSystemAbstraction/Exceptions/FileNotFoundException.cs @@ -0,0 +1,224 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security; +using System.Security.Permissions; + +namespace FileSystemAbstraction.Exceptions +{ + // + // Summary: + // The exception that is thrown when an attempt to access a file that does not exist + // on disk fails. + // Source based on System.IO.FileNotFoundExpection + [Serializable] + public class FileNotFoundException : Exception + { + private string _fileName; + private string _message; + internal int _HResult; + + // + // Summary: + // Gets the error message that explains the reason for the exception. + // + // Returns: + // The error message. + public override string Message + { + get + { + SetMessageField(); + return _message; + } + } + + // + // Summary: + // Gets the name of the file that cannot be found. + // + // Returns: + // The name of the file, or null if no file name was passed to the constructor for + // this instance. + public string FileName + { + get + { + return _fileName; + } + } + + private void SetErrorCode(int hresult) + { + base.HResult = hresult; + } + + // + // Summary: + // Initializes a new instance of the System.IO.FileNotFoundException class with + // its message string set to a system-supplied message and its HRESULT set to COR_E_FILENOTFOUND. + public FileNotFoundException() + { + SetErrorCode(-2147024894); + } + + // + // Summary: + // Initializes a new instance of the System.IO.FileNotFoundException class with + // its message string set to message and its HRESULT set to COR_E_FILENOTFOUND. + // + // Parameters: + // message: + // A description of the error. The content of message is intended to be understood + // by humans. The caller of this constructor is required to ensure that this string + // has been localized for the current system culture. + + public FileNotFoundException(string message) + : base(message) + { + SetErrorCode(-2147024894); + } + + // + // Summary: + // Initializes a new instance of the System.IO.FileNotFoundException class with + // a specified error message and a reference to the inner exception that is the + // cause of this exception. + // + // Parameters: + // message: + // A description of the error. The content of message is intended to be understood + // by humans. The caller of this constructor is required to ensure that this string + // has been localized for the current system culture. + // + // innerException: + // The exception that is the cause of the current exception. If the innerException + // parameter is not null, the current exception is raised in a catch block that + // handles the inner exception. + + public FileNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + SetErrorCode(-2147024894); + } + + // + // Summary: + // Initializes a new instance of the System.IO.FileNotFoundException class with + // its message string set to message, specifying the file name that cannot be found, + // and its HRESULT set to COR_E_FILENOTFOUND. + // + // Parameters: + // message: + // A description of the error. The content of message is intended to be understood + // by humans. The caller of this constructor is required to ensure that this string + // has been localized for the current system culture. + // + // fileName: + // The full name of the file with the invalid image. + + public FileNotFoundException(string message, string fileName) + : base(message) + { + SetErrorCode(-2147024894); + _fileName = fileName; + } + + // + // Summary: + // Initializes a new instance of the System.IO.FileNotFoundException class with + // a specified error message and a reference to the inner exception that is the + // cause of this exception. + // + // Parameters: + // message: + // The error message that explains the reason for the exception. + // + // fileName: + // The full name of the file with the invalid image. + // + // innerException: + // The exception that is the cause of the current exception. If the innerException + // parameter is not null, the current exception is raised in a catch block that + // handles the inner exception. + + public FileNotFoundException(string message, string fileName, Exception innerException) + { + _message = message; + _fileName = fileName; + } + + private void SetMessageField() + { + if (_message == null) + { + if (_fileName != null) + { + _message = _fileName +" not found."; + } + else + { + _message = "FileNotFound"; + } + } + } + + // + // Summary: + // Returns the fully qualified name of this exception and possibly the error message, + // the name of the inner exception, and the stack trace. + // + // Returns: + // The fully qualified name of this exception and possibly the error message, the + // name of the inner exception, and the stack trace. + + public override string ToString() + { + string text = GetType().FullName + ": " + Message; + if (_fileName != null && _fileName.Length != 0) + { + text = text + Environment.NewLine + "FileName_Name:"+ _fileName; + } + + if (base.InnerException != null) + { + text = text + " ---> " + base.InnerException.ToString(); + } + + if (StackTrace != null) + { + text = text + Environment.NewLine + StackTrace; + } + + return text; + } + + + private FileNotFoundException(string fileName, int hResult) + : base(null) + { + SetErrorCode(hResult); + _fileName = fileName; + SetMessageField(); + } + + // + // Summary: + // Sets the System.Runtime.Serialization.SerializationInfo object with the file + // name and additional exception information. + // + // Parameters: + // info: + // The object that holds the serialized object data about the exception being thrown. + // + // context: + // The object that contains contextual information about the source or destination. + [SecurityCritical] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("FileNotFound_FileName", _fileName, typeof(string)); + + } + } +} \ No newline at end of file diff --git a/src/SharepointFileSystem/FileSystemAbstraction/FileSystemAbstraction.csproj b/src/SharepointFileSystem/FileSystemAbstraction/FileSystemAbstraction.csproj new file mode 100644 index 0000000..b198bd5 --- /dev/null +++ b/src/SharepointFileSystem/FileSystemAbstraction/FileSystemAbstraction.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/SharepointFileSystem/FileSystemAbstraction/IFileSystem.cs b/src/SharepointFileSystem/FileSystemAbstraction/IFileSystem.cs new file mode 100644 index 0000000..ac50755 --- /dev/null +++ b/src/SharepointFileSystem/FileSystemAbstraction/IFileSystem.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; + +namespace FileSystemAbstraction { + public interface IFileSystem + { + bool FileExists(string path); + bool FileExists(string path, long biggerThanBytes); + Stream FileCreate(string path); + Stream FileOpenRead(string path); + bool DirectoryExists(string path); + void CreateDirectory(string path); + char[] GetInvalidPathChars(); + char[] GetInvalidFileNameChars(); + string PathCombine(params string[] paths); + void CreateDirectoryIfNotExist(string path, bool hidden = false); + void FileDelete(string path); + void FileMove(string sourceFileName, string destFileName); + void DeleteDirectoryAndFiles(string filePath); + void MoveFileRemoveFolder(string sourceFileName, string destFileName); + string GetContainingFolderPath(string filePath); + string GetContainingParentFolderPath(string filePath); + string GetFileNameWithoutExtension(string filePath); + string GetFileNameFromPath(string filePath); + } +} diff --git a/src/SharepointFileSystem/FileSystemAbstraction/UploadDocsFunctions.cs b/src/SharepointFileSystem/FileSystemAbstraction/UploadDocsFunctions.cs new file mode 100644 index 0000000..cabeef4 --- /dev/null +++ b/src/SharepointFileSystem/FileSystemAbstraction/UploadDocsFunctions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FileSystemAbstraction +{ + public static class UploadDocsFunctions + { + public static string RemoveIllegalCharacters(string source, char[] listofillegalchars) + { + string regexSearch = new string(listofillegalchars); + var r = new System.Text.RegularExpressions.Regex(string.Format("[{0}]", System.Text.RegularExpressions.Regex.Escape(regexSearch))); + return r.Replace(source, ""); + } + + } +} diff --git a/src/SharepointFileSystem/MSGraphAuth/AuthenticationConfig.cs b/src/SharepointFileSystem/MSGraphAuth/AuthenticationConfig.cs new file mode 100644 index 0000000..ecdad01 --- /dev/null +++ b/src/SharepointFileSystem/MSGraphAuth/AuthenticationConfig.cs @@ -0,0 +1,88 @@ + +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Web; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace MSGraphAuth { + /// + /// Description of the configuration of an AzureAD public client application (desktop/mobile application). This should + /// match the application registration done in the Azure portal + /// + internal class AuthenticationConfig { + /// + /// instance of Azure AD, for example public Azure or a Sovereign cloud (Azure China, Germany, US government, etc ...) + /// + public string Instance { get; set; } = "https://login.microsoftonline.com/{0}"; + + /// + /// Graph API endpoint, could be public Azure (default) or a Sovereign cloud (US government, etc ...) + /// + public string ApiUrl { get; set; } = "https://graph.microsoft.com/"; + + /// + /// The Tenant is: + /// - either the tenant ID of the Azure AD tenant in which this application is registered (a guid) + /// or a domain name associated with the tenant + /// - or 'organizations' (for a multi-tenant application) + /// + public string Tenant { get; set; } + + /// + /// Guid used by the application to uniquely identify itself to Azure AD + /// + public string ClientId { get; set; } + + /// + /// URL of the authority + /// + public string Authority { + get { + return String.Format(CultureInfo.InvariantCulture, Instance, Tenant); + } + } + + /// + /// Client secret (application password) + /// + /// Daemon applications can authenticate with AAD through two mechanisms: ClientSecret + /// (which is a kind of application password: this property) + /// or a certificate previously shared with AzureAD during the application registration + /// (and identified by the Certificate property belows) + /// + public string ClientSecret { get; set; } + + /// + /// The description of the certificate to be used to authenticate your application. + /// + /// Daemon applications can authenticate with AAD through two mechanisms: ClientSecret + /// (which is a kind of application password: the property above) + /// or a certificate previously shared with AzureAD during the application registration + /// (and identified by this CertificateDescription) + /// + public CertificateDescription Certificate { get; set; } + public string TenantId { get; set; } + + /// + /// Reads the configuration from a json file + /// + /// Path to the configuration json file + /// AuthenticationConfig read from the json file + public static AuthenticationConfig ReadFromJsonFile(string path) { + IConfigurationRoot Configuration; + + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(path); + + Configuration = builder.Build(); + return Configuration.Get(); + } + } + + + +} + diff --git a/src/SharepointFileSystem/MSGraphAuth/MSGraphAuth.csproj b/src/SharepointFileSystem/MSGraphAuth/MSGraphAuth.csproj new file mode 100644 index 0000000..bfb8f34 --- /dev/null +++ b/src/SharepointFileSystem/MSGraphAuth/MSGraphAuth.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/SharepointFileSystem/MSGraphAuth/OAuth2ClientCredentialsGrantService.cs b/src/SharepointFileSystem/MSGraphAuth/OAuth2ClientCredentialsGrantService.cs new file mode 100644 index 0000000..b0cf835 --- /dev/null +++ b/src/SharepointFileSystem/MSGraphAuth/OAuth2ClientCredentialsGrantService.cs @@ -0,0 +1,243 @@ + +using Azure.Core; +using Azure.Identity; +using CommunityToolkit.Diagnostics; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; + +namespace MSGraphAuth { + public class OAuth2ClientCredentialsGrantService { + private readonly IEnumerable scopes; + private readonly AuthenticationConfig config; + private readonly TokenCredential? tokenCredential; + + public string Authtoken { get; set; } + private IConfidentialClientApplication app; + + /// + /// Constructs a new . + /// + /// MS Graph application clientid uid + /// MS Graph application clientsecret + /// List of scopes for the authentication context + /// combined with tenant to create Authority Uri + /// The base service URL. For example, "https://graph.microsoft.com/v1.0" + public OAuth2ClientCredentialsGrantService(string? clientid, string? clientsecret, string? instance, string? tenant, string? tenantid, string? apiUrl, IEnumerable? scopes = null) { + // With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the + // application permissions need to be set statically (in the portal or by PowerShell), and then granted by + // a tenant administrator. + // Message "AADSTS1002012: The provided value for scope Sites.Manage.All offline_access openid profile User.Read is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI). + this.scopes = scopes?.ToArray() ?? new string[] { $"{apiUrl}.default" }; + //ThrowHelper.ThrowArgumentNullException(clientid, nameof(clientid)); + //ThrowHelper.ThrowArgumentNullException(clientsecret, nameof(clientsecret)); + //ThrowHelper.ThrowArgumentNullException(instance, nameof(instance)); + //ThrowHelper.ThrowArgumentNullException(tenant, nameof(tenant)); + //ThrowHelper.ThrowArgumentNullException(apiUrl, nameof(apiUrl)); + config = new AuthenticationConfig { + ClientId = clientid, + ClientSecret = clientsecret, + Instance = instance, + Tenant = tenant, + ApiUrl = apiUrl, + TenantId = tenantid, + }; + } + public GraphServiceClient GetClientSecretClient() { + + var options = new TokenCredentialOptions { + AuthorityHost = AzureAuthorityHosts.AzurePublicCloud + }; + + // https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential + var clientSecretCredential = new ClientSecretCredential( + config.TenantId, config.ClientId, config.ClientSecret, options); + + var graphClient = new GraphServiceClient(clientSecretCredential, scopes); + return graphClient; + } + + public GraphServiceClient GetTokenClient() { + // using Azure.Identity; + var options = new TokenCredentialOptions { + AuthorityHost = AzureAuthorityHosts.AzurePublicCloud + }; + + // https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential + var clientSecretCredential = new ClientSecretCredential( + config.TenantId, config.ClientId, config.ClientSecret, options); + + var graphClient = new GraphServiceClient(clientSecretCredential, scopes); + return graphClient; + } + public async Task GetTokenClientStoreTokenAsync() { + var options = new ClientSecretCredentialOptions { + TokenCachePersistenceOptions = new TokenCachePersistenceOptions { UnsafeAllowUnencryptedStorage = true } + }; + var creds = new ClientSecretCredential( + config.TenantId, config.ClientId, config.ClientSecret + ); + var graphServiceClient = new GraphServiceClient(creds); + return graphServiceClient; + } + + public async Task ConnectSetTokenAsync() { + + // Even daemon application is a confidential client application + app = ConfidentialClientApplicationBuilder.Create(config.ClientId) + //IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.ClientId) + .WithClientSecret(config.ClientSecret) + .WithAuthority(new Uri(config.Authority)) + .Build(); + + app.AddInMemoryTokenCache(); + + AuthenticationResult result = null; + try { + result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + } + catch (MsalUiRequiredException ex) { + // The application doesn't have sufficient permissions. + // - Did you declare enough app permissions during app creation? + // - Did the tenant admin grant permissions to the application? Admin will need to go here: + throw new Exception($"Admin to go here: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={config.ClientId}&response_type=code&redirect_uri=http://localhost/myapp/&response_mode=query&scope=openid%20offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Fmail.read&state=12345", ex);//TODO serialize scopes + throw; + } + catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) { + // Invalid scope. The scope has to be in the form "https://resourceurl/.default" + // Mitigation: Change the scope to be as expected. + throw; + } + catch (Exception ex) { + throw; + } + Authtoken = result.AccessToken; + + // With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the + // application permissions need to be set statically (in the portal or by PowerShell), and then granted by + // a tenant administrator. + //string[] scopes = new string[] { $"{config.ApiUrl}.default" }; // Generates a scope -> "https://graph.microsoft.com/.default" + + //// Call MS graph using the Graph SDK + //await CallMSGraphUsingGraphSDK(app, scopes); + + //// Call MS Graph REST API directly + //await CallMSGraph(config, app, scopes); + + return app; + } + + /* + public async Task GetInteractiveClientAsync() { + var interactiveBrowserCredentialOptions = new InteractiveBrowserCredentialOptions { + TenantId = config.TenantId, + ClientId = config.ClientId, + }; + var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveBrowserCredentialOptions); + var graphServiceClient = new GraphServiceClient(interactiveBrowserCredential); + return graphServiceClient; + } + /// + /// Calls MS Graph REST API using an authenticated Http client + /// + /// + /// + /// + /// + private static async Task CallMSGraph(AuthenticationConfig config, IConfidentialClientApplication app, string[] scopes) { + AuthenticationResult result = null; + try { + result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Token acquired"); + Console.ResetColor(); + } + catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) { + // Invalid scope. The scope has to be of the form "https://resourceurl/.default" + // Mitigation: change the scope to be as expected + throw new Exception("Scope provided is not supported", ex); + } + + // The following example uses a Raw Http call + if (result != null) { + var httpClient = new HttpClient(); + var apiCaller = new ProtectedApiCallHelper(httpClient); + await apiCaller.CallWebApiAndProcessResultASync($"{config.ApiUrl}v1.0/users", result.AccessToken, Display); + + } + } + + /// + /// The following example shows how to initialize the MS Graph SDK + /// + /// + /// + /// + private static async Task CallMSGraphUsingGraphSDK(IConfidentialClientApplication app, string[] scopes) { + // Prepare an authenticated MS Graph SDK client + GraphServiceClient graphServiceClient = GetAuthenticatedGraphClient(app, scopes); + + + List allUsers = new List(); + + try { + + var users = await graphServiceClient.Users.Request().Count(); + Console.WriteLine($"Found {users.Count()} users in the tenant"); + } + catch (ServiceException e) { + Console.WriteLine("We could not retrieve the user's list: " + $"{e}"); + } + + } + + + /// + /// An example of how to authenticate the Microsoft Graph SDK using the MSAL library + /// + /// + private static GraphServiceClient GetAuthenticatedGraphClient(IConfidentialClientApplication app, string[] scopes) { + + GraphServiceClient graphServiceClient = + new GraphServiceClient("https://graph.microsoft.com/V1.0/", new DelegateAuthenticationProvider(async (requestMessage) => { + // Retrieve an access token for Microsoft Graph (gets a fresh token if needed). + AuthenticationResult result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + + // Add the access token in the Authorization header of the API request. + requestMessage.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", result.AccessToken); + })); + + return graphServiceClient; + } + + public GraphServiceClient GetAuthenticatedGraphClientv2(Uri uri, Dictionary additionalAuthenticationContext = default, + CancellationToken cancellationToken = default) { + var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new MSGraphAuth.TokenProvider()); + var graphServiceClient = new GraphServiceClient(authenticationProvider); + return graphServiceClient; + } + + /// + /// Display the result of the Web API call + /// + /// Object to display + private static void Display(JsonNode result) { + JsonArray nodes = ((result as JsonObject).ToArray()[1]).Value as JsonArray; + + foreach (JsonObject aNode in nodes.ToArray()) { + foreach (var property in aNode.ToArray()) { + Console.WriteLine($"{property.Key} = {property.Value?.ToString()}"); + } + Console.WriteLine(); + } + } + */ + + } +} + diff --git a/src/SharepointFileSystem/MSGraphAuth/ProtectedApiCallHelper.cs b/src/SharepointFileSystem/MSGraphAuth/ProtectedApiCallHelper.cs new file mode 100644 index 0000000..2b6867f --- /dev/null +++ b/src/SharepointFileSystem/MSGraphAuth/ProtectedApiCallHelper.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2 + +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + + +namespace MSGraphAuth { + /// + /// Helper class to call a protected API and process its result + /// + internal class ProtectedApiCallHelper { + /// + /// Constructor + /// + /// HttpClient used to call the protected API + public ProtectedApiCallHelper(HttpClient httpClient) { + HttpClient = httpClient; + } + + protected HttpClient HttpClient { get; private set; } + + + /// + /// Calls the protected web API and processes the result + /// + /// URL of the web API to call (supposed to return Json) + /// Access token used as a bearer security token to call the web API + /// Callback used to process the result of the call to the web API + public async Task CallWebApiAndProcessResultASync(string webApiUrl, string accessToken, Action processResult) { + if (!string.IsNullOrEmpty(accessToken)) { + var defaultRequestHeaders = HttpClient.DefaultRequestHeaders; + if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json")) { + HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + HttpResponseMessage response = await HttpClient.GetAsync(webApiUrl); + if (response.IsSuccessStatusCode) { + string json = await response.Content.ReadAsStringAsync(); + JsonNode result = JsonNode.Parse(json); + Console.ForegroundColor = ConsoleColor.Gray; + processResult(result); + } + else { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Failed to call the web API: {response.StatusCode}"); + string content = await response.Content.ReadAsStringAsync(); + + // Note that if you got reponse.Code == 403 and reponse.content.code == "Authorization_RequestDenied" + // this is because the tenant admin as not granted consent for the application to call the Web API + Console.WriteLine($"Content: {content}"); + } + Console.ResetColor(); + } + } + } + } diff --git a/src/SharepointFileSystem/MSGraphAuth/TokenProvider.cs b/src/SharepointFileSystem/MSGraphAuth/TokenProvider.cs new file mode 100644 index 0000000..91dec30 --- /dev/null +++ b/src/SharepointFileSystem/MSGraphAuth/TokenProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.Kiota.Abstractions.Authentication; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSGraphAuth { + public class TokenProvider : IAccessTokenProvider { + public TokenProvider() { + } + + public Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = default, + CancellationToken cancellationToken = default) { + var token = "token"; + // get the token and return it in your own way + return Task.FromResult(token); + } + + public AllowedHostsValidator? AllowedHostsValidator { get; } + } +} diff --git a/src/SharepointFileSystem/Sharepoint.IO/FileServices.cs b/src/SharepointFileSystem/Sharepoint.IO/FileServices.cs new file mode 100644 index 0000000..dfc19b9 --- /dev/null +++ b/src/SharepointFileSystem/Sharepoint.IO/FileServices.cs @@ -0,0 +1,87 @@ +using FileSystemAbstraction; + +namespace Sharepoint.IO; + +public class FileServices : IFileSystem { + private const string FILESERVICE = "Sharepoint.IO.FileServices"; + private readonly ISharepointHelperService sharepointHelperService; + private readonly string sharepointSiteId; + private readonly string siteUriPart; + + public FileServices(ISharepointHelperService sharepointHelperService, string sharepointSiteId, string siteUriPart) { + this.sharepointHelperService = sharepointHelperService; + this.sharepointSiteId = sharepointSiteId; + this.siteUriPart = siteUriPart; + } + public bool FileExists(string path) { + throw new NotImplementedException(); + } + + public bool FileExists(string path, long biggerThanBytes) { + throw new NotImplementedException(); + } + + public Stream FileCreate(string path) { + throw new NotImplementedException(); + } + + public Stream FileOpenRead(string path) { + throw new NotImplementedException(); + } + + public bool DirectoryExists(string path) { + throw new NotImplementedException(); + } + + public void CreateDirectory(string path) { + throw new NotImplementedException(); + } + + public char[] GetInvalidPathChars() { + throw new NotImplementedException(); + } + + public char[] GetInvalidFileNameChars() { + throw new NotImplementedException(); + } + + public string PathCombine(params string[] paths) { + throw new NotImplementedException(); + } + + public void CreateDirectoryIfNotExist(string path, bool hidden = false) { + throw new NotImplementedException(); + } + + public void FileDelete(string path) { + throw new NotImplementedException(); + } + + public void FileMove(string sourceFileName, string destFileName) { + throw new NotImplementedException(); + } + + public void DeleteDirectoryAndFiles(string filePath) { + throw new NotImplementedException(); + } + + public void MoveFileRemoveFolder(string sourceFileName, string destFileName) { + throw new NotImplementedException(); + } + + public string GetContainingFolderPath(string filePath) { + throw new NotImplementedException(); + } + + public string GetContainingParentFolderPath(string filePath) { + throw new NotImplementedException(); + } + + public string GetFileNameWithoutExtension(string filePath) { + throw new NotImplementedException(); + } + + public string GetFileNameFromPath(string filePath) { + throw new NotImplementedException(); + } +} diff --git a/src/SharepointFileSystem/Sharepoint.IO/Sharepoint.IO.csproj b/src/SharepointFileSystem/Sharepoint.IO/Sharepoint.IO.csproj new file mode 100644 index 0000000..805d092 --- /dev/null +++ b/src/SharepointFileSystem/Sharepoint.IO/Sharepoint.IO.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/src/SharepointFileSystem/Sharepoint.IO/SharepointHelperService.cs b/src/SharepointFileSystem/Sharepoint.IO/SharepointHelperService.cs new file mode 100644 index 0000000..4c14439 --- /dev/null +++ b/src/SharepointFileSystem/Sharepoint.IO/SharepointHelperService.cs @@ -0,0 +1,33 @@ +using Microsoft.Graph; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sharepoint.IO { + + public interface ISharepointHelperService { + Task GetSiteAsync(string siteid); + } + + public class SharepointHelperService : ISharepointHelperService { + private readonly GraphServiceClient graphClient; + + public SharepointHelperService(GraphServiceClient graphClient) { + this.graphClient = graphClient; + } + + /// + /// https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/7a2be45d2cf37f18a32cc9a60d0edf441fd23a08/docs/v4-reference-docs/site-get.md + /// + /// + /// + public async Task GetSiteAsync(string siteid) { + var site = await graphClient.Sites[$"{siteid}"] + .GetAsync(); + return site?.Sites?.First(); + } + } + +} diff --git a/src/SharepointFileSystem/WindowsLocalFileSystem/FileServices.cs b/src/SharepointFileSystem/WindowsLocalFileSystem/FileServices.cs new file mode 100644 index 0000000..fac77b2 --- /dev/null +++ b/src/SharepointFileSystem/WindowsLocalFileSystem/FileServices.cs @@ -0,0 +1,159 @@ +using FileSystemAbstraction; + +namespace WindowsLocalFileSystem { + public class FileServices : IFileSystem { + private const string FILESERVICE = "WindowsLocalFileSystem.FileServices"; + + public bool FileExists(string path) { + return File.Exists(path); + } + + public bool FileExists(string path, long biggerThanBytes) { + var fileinfo = new FileInfo(path); + return (File.Exists(path) && fileinfo.Length > biggerThanBytes); + } + + public Stream FileCreate(string path) { + return File.Create(path); + } + + public bool DirectoryExists(string path) { + return Directory.Exists(path); + } + + public void CreateDirectory(string path) { + Directory.CreateDirectory(path); + } + + public char[] GetInvalidPathChars() { + return Path.GetInvalidPathChars(); + } + + public char[] GetInvalidFileNameChars() { + return Path.GetInvalidFileNameChars(); + } + public string PathCombine(params string[] paths) { + //never returns on NetStandard2.0 + return Path.Combine(paths); + } + public System.IO.Stream FileOpenRead(string path) { + return File.OpenRead(path); + } + + public void CreateDirectoryIfNotExist(string path, bool hidden = false) { + var fileInfo = new FileInfo(path); + var destFolder = fileInfo.Directory; + try { + if (destFolder != null && !this.DirectoryExists(destFolder.FullName)) { + destFolder.Create(); + if (hidden) + destFolder.Attributes = destFolder.Attributes & FileAttributes.Hidden; + } + } + catch (Exception ex) { + throw new Exception($"Unable to access: {path}", ex); + } + } + + public void FileDelete(string path) { + File.Delete(path); + } + + public void FileMove(string sourceFileName, string destFileName) { + File.Move(sourceFileName, destFileName); + } + + public void DeleteDirectoryAndFiles(string filePath) { + var fileInfo = new FileInfo(filePath); + var destFolder = fileInfo.Directory; + if (destFolder != null && !this.DirectoryExists(destFolder.FullName)) + return; + + try { + if (File.Exists(filePath)) { + File.Delete(filePath); + fileInfo = new FileInfo(filePath); + destFolder = fileInfo.Directory; + } + } + catch (Exception ex) { + throw new Exception($"Unable to delete file ('{filePath}')", ex); + } + bool removefailed = false; + try { + destFolder.Delete(); + } + catch { + removefailed = true; + } + if (removefailed) { + //wait and try again + System.Threading.Thread.Sleep(3000); + } + try { + if (destFolder.Exists) + destFolder.Delete(); + } + catch (Exception ex) { + throw new Exception($"Unable to remove directory('{destFolder}')", ex); + } + } + + public void MoveFileRemoveFolder(string sourceFileName, string destFileName) { + + var srcFileInfo = new FileInfo(sourceFileName); + if (srcFileInfo.Directory == null) throw new NullReferenceException("Folder not found"); + var srcFileInfoDirectory = srcFileInfo.Directory.ToString(); + try { + if (File.Exists(sourceFileName) && !File.Exists(destFileName)) { + File.Move(sourceFileName, destFileName); + } + else if (File.Exists(sourceFileName) && File.Exists(destFileName)) { + File.Delete(sourceFileName); + } + + srcFileInfo = new FileInfo(sourceFileName); + } + catch (Exception ex) { + throw new Exception($"Error during cleaning temp files during move '{sourceFileName}' to '{destFileName}'", ex); + } + + //remove temp folder + try { + if (srcFileInfo.Directory != null && srcFileInfo.Directory.Exists) + srcFileInfo.Directory.Delete(); + } + catch (Exception ex) { + throw new Exception($"Unable to remove directory('{srcFileInfoDirectory}')", ex); + } + } + + public string GetContainingFolderPath(string filePath) { + var fileInfo = new FileInfo(filePath); + if (fileInfo.Directory == null) throw new NullReferenceException("Folder not found"); + var parentDirectoryPath = fileInfo.Directory.ToString(); + return parentDirectoryPath; + } + + public string GetContainingParentFolderPath(string filePath) { + var fileInfo = new FileInfo(filePath); + if (fileInfo.Directory == null) throw new NullReferenceException("Folder not found"); + if (fileInfo.Directory.Parent == null) throw new NullReferenceException("Folder Parent not found"); + var parentParentDirectoryPath = fileInfo.Directory.Parent.FullName; + return parentParentDirectoryPath; + } + + public string GetFileNameWithoutExtension(string filePath) { + return Path.GetFileNameWithoutExtension(filePath); + } + + public string GetFileNameFromPath(string filePath) { + try { + // only works for filesystem paths, URLs fail + var f = new FileInfo(filePath); + return f.Name; + } + catch { return filePath; } + } + } +} diff --git a/src/SharepointFileSystem/WindowsLocalFileSystem/WindowsLocalFileSystem.csproj b/src/SharepointFileSystem/WindowsLocalFileSystem/WindowsLocalFileSystem.csproj new file mode 100644 index 0000000..0e62714 --- /dev/null +++ b/src/SharepointFileSystem/WindowsLocalFileSystem/WindowsLocalFileSystem.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/SharepointFileSystem/dotnet-console-microsoft-graph.sln b/src/SharepointFileSystem/dotnet-console-microsoft-graph.sln new file mode 100644 index 0000000..5c4255a --- /dev/null +++ b/src/SharepointFileSystem/dotnet-console-microsoft-graph.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33417.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-console-microsoft-graph", "0Console\dotnet-console-microsoft-graph.csproj", "{8D619F0F-45D6-481E-AC2B-2B78CA039B2E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileSystemAbstraction", "FileSystemAbstraction\FileSystemAbstraction.csproj", "{17F0C64C-0219-410F-B895-D15DBFD1BD96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsLocalFileSystem", "WindowsLocalFileSystem\WindowsLocalFileSystem.csproj", "{82E69ED4-1958-45E8-A903-B2F05FE61002}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharepoint.IO", "Sharepoint.IO\Sharepoint.IO.csproj", "{008F9F20-345E-4D44-8FDF-463456FFD255}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSGraphAuth", "MSGraphAuth\MSGraphAuth.csproj", "{C0CFA446-6529-41F1-AE32-BA0BAC84A2DF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D619F0F-45D6-481E-AC2B-2B78CA039B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D619F0F-45D6-481E-AC2B-2B78CA039B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D619F0F-45D6-481E-AC2B-2B78CA039B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D619F0F-45D6-481E-AC2B-2B78CA039B2E}.Release|Any CPU.Build.0 = Release|Any CPU + {17F0C64C-0219-410F-B895-D15DBFD1BD96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17F0C64C-0219-410F-B895-D15DBFD1BD96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17F0C64C-0219-410F-B895-D15DBFD1BD96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17F0C64C-0219-410F-B895-D15DBFD1BD96}.Release|Any CPU.Build.0 = Release|Any CPU + {82E69ED4-1958-45E8-A903-B2F05FE61002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82E69ED4-1958-45E8-A903-B2F05FE61002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82E69ED4-1958-45E8-A903-B2F05FE61002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82E69ED4-1958-45E8-A903-B2F05FE61002}.Release|Any CPU.Build.0 = Release|Any CPU + {008F9F20-345E-4D44-8FDF-463456FFD255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {008F9F20-345E-4D44-8FDF-463456FFD255}.Debug|Any CPU.Build.0 = Debug|Any CPU + {008F9F20-345E-4D44-8FDF-463456FFD255}.Release|Any CPU.ActiveCfg = Release|Any CPU + {008F9F20-345E-4D44-8FDF-463456FFD255}.Release|Any CPU.Build.0 = Release|Any CPU + {C0CFA446-6529-41F1-AE32-BA0BAC84A2DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0CFA446-6529-41F1-AE32-BA0BAC84A2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0CFA446-6529-41F1-AE32-BA0BAC84A2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0CFA446-6529-41F1-AE32-BA0BAC84A2DF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {42C72E36-71C7-4E59-8366-52D432A3C2AB} + EndGlobalSection +EndGlobal