Issues Implementing Advanced Features with Microsoft Graph SDK 2.0 for SharePoint
Hello everyone,
I am working on a project that uses version 2.0 of the Microsoft Graph SDK library to interact with SharePoint. I have successfully set up the following permissions in Azure AD:
Files.ReadWrite.All BrowserSiteLists.ReadWrite.All Sites.FullControl.All And I have developed a class, GraphTest, to handle authentication and some basic operations. My current code works for obtaining a SharePoint site ID via URL (certainly not the best method), but I am encountering difficulties in implementing more advanced features, specifically:
- Use the "Get a site resource" method with hostname and relative-path (GET /sites/{hostname}:/{server-relative-path}).
- Create a new folder in SharePoint by specifying a destination URL and the folder name (POST /sites/{site-id}/drive/items/{parent-item-id}/children).
- Upload a new small file to SharePoint by providing a destination URL and the file (PUT /sites/{site-id}/drive/items/{parent-id}:/{filename}:/content).
- Upload a large file to SharePoint by creating an upload session (POST /sites/{siteId}/drive/items/{itemId}/createUploadSession).
I am a bit confused by the documentation and the examples provided, and would be very grateful if someone could provide me with some code examples or advice on how to implement these features. I am sure that other members of the community could also benefit from this discussion.
Thank you in advance for your time and help.
Best regards, Ivan
Here is my code:
// Define the SharePoint site URL $url = 'https://contoso.sharepoint.com/sites/examplesite';
// Define authentication settings including client ID, client secret, and tenant ID $settings = array( 'clientId' => 'xxxxxxxxxxxxxxxxxxxx', 'clientSecret' => 'xxxxxxxxxxxxxxxxxxxx', 'tenantId' => 'xxxxxxxxxxxxxxxxxxxx' );
// Create an instance of the GraphTest class and initialize it with the authentication settings $graphServiceClient = new GraphTest($settings);
// Call the getSiteId method of the GraphTest instance to obtain the SharePoint site ID based on the URL $resultID = $graphServiceClient->getSiteId($url);
class GraphTest { private $graphServiceClient; private $tenantId; private $clientId; private $clientSecret;
/**
* Constructor to initialize class properties.
*
* @param array $settings Configuration settings including tenantId, clientId, and clientSecret.
*/
public function __construct(array $settings)
{
$this->tenantId = $settings['tenantId'];
$this->clientId = $settings['clientId'];
$this->clientSecret = $settings['clientSecret'];
$this->authenticate();
}
/**
* Authenticates and sets up the Graph Service Client.
*/
private function authenticate()
{
$tokenRequestContext = new ClientCredentialContext($this->tenantId, $this->clientId, $this->clientSecret);
$this->graphServiceClient = new GraphServiceClient($tokenRequestContext);
}
/**
* Retrieves the site ID based on a given SharePoint path.
*
* @param string $sharePointPath The SharePoint URL.
* @return string|null The site ID or null if not found.
*/
public function getSiteId($sharePointPath)
{
try {
$siteName = basename(parse_url($sharePointPath, PHP_URL_PATH));
$request = $this->graphServiceClient->sites()->get()->wait();
foreach ($request->getValue() as $site) {
if ($site->getName() === $siteName) {
return $site->getId();
}
}
return null;
} catch (ApiException $ex) {
// Log the error and rethrow or handle it as needed
error_log("API Error: " . $ex->getMessage());
throw $ex;
} catch (\Exception $e) {
// Log general exceptions and rethrow or handle them as needed
error_log("General Error: " . $e->getMessage());
throw $e;
}
}
}
Hello everyone,
I wish to provide an update regarding the request I previously opened. I have successfully implemented the functionalities for deleting and uploading files, using some fundamental functions to achieve the final result. However, I am encountering an issue in the creation of folders. When I try to create a new folder, I receive the following error:
[item] Read called with an open stream or textreader. Please close any open streams or text readers before calling Read.
Interestingly, if I upload a file specifying a path of non-existent folders, the system automatically creates all the necessary folders along that path. I would appreciate help in resolving the issue related to the creation of folders. Here is the code I am using:
$urlSharepoint = 'https://contoso.sharepoint.com/sites/examplesite';
// Definition of authentication settings including client ID, client secret, and tenant ID $settings = array( 'clientId' => 'xxxxxxxxxxxxxxxxxxxx', 'clientSecret' => 'xxxxxxxxxxxxxxxxxxxx', 'tenantId' => 'xxxxxxxxxxxxxxxxxxxx', 'urlSharepoint' => $urlSharepoint );
// Creating an instance of the GraphTest class and initializing it with authentication settings $graphServiceClient = new GraphTest($settings);
$folderName = "newFolder"; $graphServiceClient = new GraphAssihub($settings); $resultFolder = $graphServiceClient->createFolder($folderName);
// Uploading a small file $sourceFilePathClaims = "path_file/file.txt"; $smallFileUploadResponse = $graphServiceClient->uploadFile($sourceFilePathClaims, 'folderSharepoint/file.txt');
// Uploading a large file $sourceFilePathClaims = "path_file/file2.txt"; $LargeFileUploadResponse = $graphServiceClient->uploadFile($sourceFilePathClaims, 'folderSharepoint/file2.txt');
$resultItemId = $graphServiceClient->deleteFile('/folderSharepoint/file.txt'); $resultItemId = $graphServiceClient->deleteFile('/folderSharepoint/file2.txt'); $resultItemId = $graphServiceClient->deleteFile('/folderSharepoint/');
Code:
use Microsoft\Graph\Generated\Models\DriveItem; use Microsoft\Graph\Generated\Models\Folder; use Microsoft\Graph\GraphServiceClient; use Microsoft\Kiota\Abstractions\ApiException; use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; use GuzzleHttp\Psr7\Utils;
class GraphAssihub { // Private variables to store credentials and configurations private $graphServiceClient; private $tenantId; private $clientId; private $clientSecret; private $urlSharepoint; private $siteId; private $driveId; private $graphApiBaseUrl; // Variable for the base URL of Microsoft Graph API
// Constructor to initialize the object with necessary settings
public function __construct($settings)
{
// Assigning the settings from a configuration array
$this->tenantId = $settings['tenantId'];
$this->clientId = $settings['clientId'];
$this->clientSecret = $settings['clientSecret'];
$this->urlSharepoint = $settings['urlSharepoint'];
$this->graphApiBaseUrl = 'https://graph.microsoft.com/v1.0/'; // Set the base URL for Graph API
// Authenticate and initialize site and drive IDs
$this->authenticate();
$this->siteId = $this->getSiteId($this->urlSharepoint);
$this->driveId = $this->getDriveId($this->siteId);
}
// Authenticate with Microsoft Graph API using client credentials
private function authenticate()
{
// Create a context for token request using client credentials
$tokenRequestContext = new ClientCredentialContext($this->tenantId, $this->clientId, $this->clientSecret);
// Initialize GraphServiceClient with the token context
$this->graphServiceClient = new GraphServiceClient($tokenRequestContext);
}
// Get the Site ID from the SharePoint URL
private function getSiteId($sharePointUrl)
{
try {
// Validate the provided URL
if (!filter_var($sharePointUrl, FILTER_VALIDATE_URL)) {
throw new Exception("Invalid URL.");
}
// Extract domain and site name from URL
$parsedUrl = parse_url($sharePointUrl);
if (!isset($parsedUrl['host']) || !isset($parsedUrl['path'])) {
throw new Exception("URL missing necessary components (domain or path).");
}
$domain = $parsedUrl['host'];
$siteName = basename($parsedUrl['path']);
// Check if the site name is not empty
if (empty($siteName)) {
throw new Exception("Site name cannot be empty.");
}
// Construct the final URL for Microsoft Graph API using the base URL
$finalSharePointPath = $this->graphApiBaseUrl . "sites/{$domain}:/sites/{$siteName}:/";
// Execute the request to Microsoft Graph API to obtain the site ID
$response = $this->graphServiceClient->sites()->withUrl($finalSharePointPath)->get()->wait();
// Return the site ID if available
return $response->getAdditionalData()['id'];
} catch (ApiException $ex) {
// Handle specific API exceptions
echo "API Error: " . $ex->getMessage();
} catch (\Exception $e) {
// Handle other generic exceptions
throw new Exception("General Error: " . $e->getMessage());
}
// Return null if site ID is not found or if an error occurs
return null;
}
// Method to get the Drive ID
private function getDriveId()
{
try {
// Execute the request to get drive information
$response = $this->graphServiceClient->sites()->bySiteId($this->siteId)->drive()->get()->wait();
// Extract and return the driveId from the response
return $response->getId();
} catch (ApiException $e) {
// Handle API exceptions
return null;
}
}
// Method to get the item ID for a given SharePoint path
private function getItemId($sharePointPath)
{
try {
// Construct the final URL for Microsoft Graph API using the base URL
$sharePointPathFinal = $this->graphApiBaseUrl . "drives/" . $this->driveId . "/root:" . $sharePointPath;
$response = $this->graphServiceClient->drives()->byDriveId($this->driveId)->withUrl($sharePointPathFinal)->get()->wait();
// Extract the item ID from the response
return $response->getId();
} catch (ApiException $e) {
// Handle exceptions
throw new Exception("Error in finding the folder item: " . $e->getMessage());
}
}
// Method to delete a file from SharePoint
public function deleteFile($sharePointPath)
{
try {
// Get the item ID from the SharePoint path
$itemId = $this->getItemId($sharePointPath);
// Construct the final URL for the Microsoft Graph API
$response = $this->graphServiceClient->drives()->byDriveId($this->driveId)->items()->byDriveItemId($itemId)->delete()->wait();
// Return the response (usually null for a successful delete operation)
return $response;
} catch (ApiException $e) {
// Handle exceptions related to the API
throw new Exception("Error in finding the folder item: " . $e->getMessage());
}
}
// Method to upload a file to SharePoint
public function uploadFile($filePath, $destinationPath)
{
try {
// Assume that the parent item ID is 'root' (this may vary based on your structure)
$parentItemId = 'root';
$driveItemId = $parentItemId . ':/' . $destinationPath . ":";
// Create an input stream for the file to be uploaded
$inputStream = Utils::streamFor(fopen($filePath, 'r'));
// Execute the upload request to the Microsoft Graph API
$response = $this->graphServiceClient->drives()->byDriveId($this->driveId)->items()->byDriveItemId($driveItemId)->content()->put($inputStream)->wait();
// Return true on successful upload
return true;
} catch (ApiException $e) {
// Handle API exceptions
return null;
}
}
// Method to create a folder in SharePoint
public function createFolder($folderName)
{
try {
// Construct the URL for the parent folder
$parentFolderPath = 'root';
$requestBody = new DriveItem();
$requestBody->setName($folderName);
$folder = new Folder();
$requestBody->setFolder($folder);
$additionalData = [
'@microsoft.graph.conflictBehavior' => 'rename',
];
$requestBody->setAdditionalData($additionalData);
// Execute the folder creation request to the Microsoft Graph API
$response = $this->graphServiceClient->drives()->byDriveId($this->driveId)->items()->byDriveItemId($parentFolderPath)->children()->post($requestBody)->wait();
// Return the response object (usually contains information about the newly created folder)
return $response;
} catch (ApiException $e) {
// Handle exceptions related to the API
throw new Exception("Error in creating a folder: " . $e->getMessage());
}
}
}
For what it's worth, I'm having the exact same problem using essentially identical code. Error: "Read called with an open stream or textreader. Please close any open streams or text readers before calling Read." Is the problem that an upstream function, children() perhaps, is leaving a stream open?
+1 Can confirm the issue, creating folder throws the same error message.
From debugging I found the issue being related to how the DriveItem gets serialized - specifically how the JsonSerializationWrite handles binary content.
// in DriveItem::serialize
...
$write->writeBinaryContent('content', $this->getContent());
...
That's all fine and dandy but now we need to look at how the JsonSerializationWriter handles these values - there's 2 important methods here:
// in JsonSerializationWrite.php
public function writeStringValue(?string $key, ?string $value): void {
$propertyValue = $value !== null ? '"'.addcslashes($value, "\\\r\n\"\t").'"' : '';
if ($value !== null) {
if (!empty($key)) {
$this->writePropertyName($key);
}
$this->writePropertyValue($key, $propertyValue);
}
}
public function writeBinaryContent(?string $key, ?StreamInterface $value): void {
if ($value !== null) {
$this->writeStringValue($key, $value->getContents());
} else {
$this->writeNullValue($key);
}
}
The main take away here is that; whether you provide a null value, or a StreamInterface you either end up returning an empty string, or null - in either case, the request ends up adding "content": "" or "content": null to the actual request resulting in an error on the MS Graph API.
We had a fix for this originally where we provided a custom StreamInterface which would return null on getContents - but a recent update added the return type of string to StreamInterface::getContents which prevented this.
Another (somewhat similar) approach is to provide a custom DriveItem class instead of the base one. All we want to do is modify the serialize method:
use \Microsoft\Graph\Generated\Models\BaseItem;
use \Microsoft\Graph\Generated\Models\DriveItem;
class NullSafeDriveItem extends DriveItem {
/**
* Serializes information the current object
* @param SerializationWriter $writer Serialization writer to use to serialize this model
*/
public function serialize(SerializationWriter $writer): void {
/* vvv IMPORTANT BIT vvv */
BaseItem::serialize($writer);
/* ^^^ IMPORTANT BIT ^^^ */
$writer->writeObjectValue('analytics', $this->getAnalytics());
$writer->writeObjectValue('audio', $this->getAudio());
$writer->writeObjectValue('bundle', $this->getBundle());
$writer->writeCollectionOfObjectValues('children', $this->getChildren());
/* vvv IMPORTANT BIT vvv */
$content = $this->getContent();
if ($content !== null) {
$writer->writeBinaryContent('content', $this->getContent());
}
/* ^^^ IMPORTANT BIT ^^^ */
$writer->writeStringValue('cTag', $this->getCTag());
$writer->writeObjectValue('deleted', $this->getDeleted());
$writer->writeObjectValue('file', $this->getFile());
$writer->writeObjectValue('fileSystemInfo', $this->getFileSystemInfo());
$writer->writeObjectValue('folder', $this->getFolder());
$writer->writeObjectValue('image', $this->getImage());
$writer->writeObjectValue('listItem', $this->getListItem());
$writer->writeObjectValue('location', $this->getLocation());
$writer->writeObjectValue('malware', $this->getMalware());
$writer->writeObjectValue('package', $this->getPackage());
$writer->writeObjectValue('pendingOperations', $this->getPendingOperations());
$writer->writeCollectionOfObjectValues('permissions', $this->getPermissions());
$writer->writeObjectValue('photo', $this->getPhoto());
$writer->writeObjectValue('publication', $this->getPublication());
$writer->writeObjectValue('remoteItem', $this->getRemoteItem());
$writer->writeObjectValue('retentionLabel', $this->getRetentionLabel());
$writer->writeObjectValue('root', $this->getRoot());
$writer->writeObjectValue('searchResult', $this->getSearchResult());
$writer->writeObjectValue('shared', $this->getShared());
$writer->writeObjectValue('sharepointIds', $this->getSharepointIds());
$writer->writeIntegerValue('size', $this->getSize());
$writer->writeObjectValue('specialFolder', $this->getSpecialFolder());
$writer->writeCollectionOfObjectValues('subscriptions', $this->getSubscriptions());
$writer->writeCollectionOfObjectValues('thumbnails', $this->getThumbnails());
$writer->writeCollectionOfObjectValues('versions', $this->getVersions());
$writer->writeObjectValue('video', $this->getVideo());
$writer->writeStringValue('webDavUrl', $this->getWebDavUrl());
$writer->writeObjectValue('workbook', $this->getWorkbook());
}
}
With that in place, we can change our create code to something like:
use \Microsoft\Graph\Generated\Models\Folder;
$body = new NullSafeDriveItem;
$body->setName("My Folder");
$body->setFolder(new Folder);
$graph_client->drives->byDriveId($drive_id)->items()->byDriveItemId($item_id)->children()->post($body)->wait();
NOTE: You cannot use
$body->setContent(null)as this will create a key in theBackingStoreofDriveItemwhich in turns sends anullvalue as a part of the request.
Hello @jaakon Is htis still an issue, From the comments, looks like the main issue now is folder creation in onedrive?