pnp / pnpjs

Fluent JavaScript API for SharePoint and Microsoft Graph REST APIs
https://pnp.github.io/pnpjs/
Other
753 stars 304 forks source link

IFolder.files() returns empty array #3077

Closed ahwitz closed 3 months ago

ahwitz commented 3 months ago

Major Version

4.x

Minor Version Number

2.0

Target environment

NodeJS

Additional environment details

NodeJS, MSAL, in Linux, Windows, or WSL on developer machines or in Azure Functions.

Expected or Desired Behavior

Folder.files() call as documented should work.

Observed Behavior

In the "Steps to reproduce" below: 1) is the printout of the folder, which does not throw any kind of error 2) is the printout of files(), which prints an empty array (same if the .getFolderByServerRelativePath().files() is chained) 3) is the printout of a call to get an individual file inside the folder, which succeeds

We'd expect the .files() call to load some sort of metadata so we can list files in the folder. Also worth noting that what is a function is referenced as a property at the end of this documentation.

Steps to Reproduce

const { KeyVaultClient } = require("./key-vault-client");

class SharepointClient {
  authorityUrl = `https://login.microsoftonline.com/{GUID}/`; // our tenant ID
  clientId = "{GUID}"; // ID of the app registration we're using
  constructor(context, site) {
    this.context = context;
    this.site = site;
    this.keyVaultClient = new KeyVaultClient();
    this.spInstance = null;
  }

  async initClient() {
    if (this.spInstance) return;

    // Set up as `await import` because of quirks with our env where we can't use the right combo of TS and MJS
    await import("@pnp/sp/presets/all.js");
    const { LogLevel, PnPLogging } = await import("@pnp/logging");
    const { Web } = await import("@pnp/sp/webs/index.js");
    const { SPDefault } = await import("@pnp/nodejs/index.js");
    await import("@pnp/sp/files/web.js");
    await import("@pnp/sp/files/folder.js");
    await import("@pnp/sp/lists/index.js");
    await import("@pnp/sp/folders/index.js");
    await import("@pnp/sp/items/index.js");
    await import("@pnp/sp/batching.js");

    // Internal class, but maps to the default azure library
    const clientCertificate = await this.keyVaultClient.getCertificate(
      "sharepoint-user-pem-certificate"
    );

    // https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread
    const configuration = {
      auth: {
        authority: this.authorityUrl,
        clientId: this.clientId,
        clientCertificate,
      },
    };

    // https://pnp.github.io/pnpjs/concepts/auth-nodejs/
    this.spInstance = Web(
      "https://[our Sharepoint site].sharepoint.com/sites/" + this.site
    )
      .using(
        SPDefault({
          msal: {
            config: configuration,
            scopes: ["https://[our Sharepoint site].sharepoint.com/.default"],
          },
        })
      )
      .using(PnPLogging(LogLevel.Warning));
  }

  async readFolder(filepath) {
    await this.initClient();

    const folder = await this.spInstance.getFolderByServerRelativePath(
      "/sites/Finance/Shared%20Documents/Economics/Reports/"
    );
    console.log("folder", folder);
    /* 1 - Prints:
  folder <ref *1> [Function (anonymous)] _Folder {
    moments: {
  ...
    */

    const files = await folder.files();
    console.log("files", files);
    /* 2 - Prints:
files []
    */

    const file = await this.spInstance.getFileByUrl(
      "/sites/Finance/Shared%20Documents/Economics/Reports/RollUp_2024_06_20.xls"
    );
    const fileBuffer = await file.getBuffer();
    console.log(fileBuffer);
    /* 3 - Prints:
ArrayBuffer {
  [Uint8Contents]: <54 61 ... more bytes>,
  byteLength: 1093243
}
*/

  }
}
(async function() {
  const client = new SharepointClient();
  await client.readFolder();
})();
bcameron1231 commented 3 months ago

Hi, I've taken a look but I haven't been able to reproduce. However, I was testing using Browser and not Node. So will continue to try and replicate.

patrick-rodgers commented 3 months ago

Did another test and just learned that if the folder is not found (i.e. the call to getFolderByServerRelativePath returns 404) you will get back an empty array of files vs a 404 when you call folder.files().

Not sure if that is what you are seeing here, but we are otherwise not able to reproduce this issue.

juliemturner commented 3 months ago

Ok, figured this out but took a bit of checking. The getFolderByServerRelativePath endpoint (and a few others) ONLY takes a non-encoded URL. So, the problem is you have %20 in the folder name... if you remove that it'll work. We will add to the docs about the endpoint needing to not be encoded.

ahwitz commented 3 months ago

Thanks, y'all - @juliemturner, I've converted the %20 to a space and still getting the same result.

Since we're dealing with character encodings, there is actually an underscore in the final folder directory, like:

const folder = await this.spInstance.getFolderByServerRelativePath(
    "/sites/Finance/Shared Documents/Economics/Source_Reports/"
);

...but other than that, the _url param is:

{
  _url: "https://nesthealthcares.sharepoint.com/sites/Finance/_api/web/getFolderByServerRelativePath(decodedUrl='%2Fsites%2FFinance%2FShared%20Documents%2FEconomics%2FNavinet_Reports%2F')"
}

Also, I tried the folderFromServerRelativePath logic that I see in the docs PR, and this structure:

    const folderBase2 = folderFromServerRelativePath(this.spInstance, filepath);
    console.log("folderBase2", folderBase2);
    const folder2 = await folderBase2();
    console.log("folder2", folder2);
    const files2 = await folder2.files();
    console.log("files2", files2);

...gives me a 404 with the same decodedUrl= URL in the debug. I'm still a bit new to Sharepoint, but if the user has access to the file (per a file in that folder loading), would they also have access to the folder, and if not, does MS obscure 401/403 as 404 like this?

juliemturner commented 3 months ago

You have a trailing / in your URL.. works fine with _ in the path.

ahwitz commented 3 months ago

That easy - we're set, can close this whenever y'all want. Thanks for the help!

github-actions[bot] commented 3 months ago

This issue is locked for inactivity or age. If you have a related issue please open a new issue and reference this one. Closed issues are not tracked.