Skip to content

Commit

Permalink
Enhances 'spo folder add' with recursive folders. Closes pnp#5887
Browse files Browse the repository at this point in the history
  • Loading branch information
Saurabh7019 authored and MathijsVerbeeck committed Jun 18, 2024
1 parent e66f763 commit 2b8cedf
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 23 deletions.
9 changes: 9 additions & 0 deletions docs/docs/cmd/spo/folder/folder-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ m365 spo folder add [options]

`--color [color]`
: Visual color of the folder. Valid values are a color name or a number. Check remarks for more info.

`--ensureParentFolders [ensureParentFolders]`
: Ensure that the parent folder path is available. Any missing folders will be created recursively.
```

<Global />
Expand Down Expand Up @@ -79,6 +82,12 @@ Create a folder with a light teal look
m365 spo folder add --webUrl https://contoso.sharepoint.com --url '/ProjectFiles/Project-x' --color lightTeal
```

Create a folder and make sure that any missing folders specified as parent folders are created.

```sh
m365 spo folder add --webUrl https://contoso.sharepoint.com/sites/project-x --parentFolderUrl '/Projects/2024/Q1/Reports' --name Financial --ensureParentFolders
```

## Response

<Tabs>
Expand Down
40 changes: 40 additions & 0 deletions src/m365/spo/commands/folder/folder-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe(commands.FOLDER_ADD, () => {
let stubPostResponses: any;

const addResponse = { "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "abc", "ProgID": null, "ServerRelativeUrl": "/sites/test1/Shared Documents/abc", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" };
const addResponseChild = { "Exists": true, "IsWOPIEnabled": false, "ItemCount": 0, "Name": "folder1", "ProgID": null, "ServerRelativeUrl": "/Shared Documents/abc/folder1", "TimeCreated": "2018-05-02T23:21:45Z", "TimeLastModified": "2018-05-02T23:21:45Z", "UniqueId": "0ac3da45-cacf-4c31-9b38-9ef3697d5a66", "WelcomePage": "" };

const webUrl = 'https://contoso.sharepoint.com';
const parentFolder = '/Shared Documents';
Expand Down Expand Up @@ -71,6 +72,7 @@ describe(commands.FOLDER_ADD, () => {

afterEach(() => {
sinonUtil.restore([
request.get,
request.post
]);
});
Expand Down Expand Up @@ -194,6 +196,44 @@ describe(commands.FOLDER_ADD, () => {
});
});

it('creates folder and ensures parent folder path', async () => {
const requestGet = sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter('Shared Documents/abc')}')?$select=Exists`) {
return {
Exists: false
};
}

throw 'Invalid request';
});

const requestPost = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso.sharepoint.com/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter('Shared Documents/abc')}')`) {
return addResponse;
}

if (opts.url === `https://contoso.sharepoint.com/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter('/Shared Documents/abc/folder1')}')`) {
return addResponseChild;
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
debug: true,
webUrl: webUrl,
parentFolderUrl: '/Shared Documents/abc',
name: 'folder1',
ensureParentFolders: true
}
});

assert.strictEqual(requestGet.lastCall.args[0].url, `https://contoso.sharepoint.com/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter('Shared Documents/abc')}')?$select=Exists`);

assert.strictEqual(requestPost.callCount, 2);
});

it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => {
const actual = await command.validate({ options: { webUrl: 'foo', parentFolderUrl: parentFolder, name: folderName } }, commandInfo);
assert.notStrictEqual(actual, true);
Expand Down
103 changes: 80 additions & 23 deletions src/m365/spo/commands/folder/folder-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Options extends GlobalOptions {
parentFolderUrl: string;
name: string;
color?: string;
ensureParentFolders?: boolean
}

class SpoFolderAddCommand extends SpoCommand {
Expand All @@ -41,7 +42,8 @@ class SpoFolderAddCommand extends SpoCommand {
#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
color: typeof args.options.color !== 'undefined'
color: typeof args.options.color !== 'undefined',
ensureParentFolders: !!args.options.ensureParentFolders
});
});
}
Expand All @@ -60,6 +62,9 @@ class SpoFolderAddCommand extends SpoCommand {
{
option: '--color [color]',
autocomplete: Object.keys(FolderColorValues)
},
{
option: '--ensureParentFolders [ensureParentFolders]'
}
);
}
Expand All @@ -82,43 +87,95 @@ class SpoFolderAddCommand extends SpoCommand {

#initTypes(): void {
this.types.string.push('webUrl', 'parentFolderUrl', 'name', 'color');
this.types.boolean.push('ensureParentFolders');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
if (this.verbose) {
await logger.logToStderr(`Adding folder to site ${args.options.webUrl}...`);
}

const parentFolderServerRelativeUrl: string = urlUtil.getServerRelativePath(args.options.webUrl, args.options.parentFolderUrl);
const serverRelativeUrl: string = `${parentFolderServerRelativeUrl}/${args.options.name}`;

const requestOptions: CliRequestOptions = {
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};

if (args.options.color === undefined) {
requestOptions.url = `${args.options.webUrl}/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}')`;
}
else {
requestOptions.url = `${args.options.webUrl}/_api/foldercoloring/createfolder(DecodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}', overwrite=false)`;
requestOptions.data = {
coloringInformation: {
ColorHex: FolderColorValues[args.options.color] || args.options.color
}
};
if (args.options.ensureParentFolders) {
await this.ensureParentFolderPath(args.options, parentFolderServerRelativeUrl, logger);
}

const folder = await request.post<FolderProperties>(requestOptions);
const folder = await this.addFolder(serverRelativeUrl, args.options, logger);
await logger.log(folder);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}

private async ensureParentFolderPath(options: Options, parentFolderPath: string, logger: Logger): Promise<void> {
if (this.verbose) {
await logger.logToStderr(`Ensuring parent folders exist...`);
}

const parsedUrl = new URL(options.webUrl);
const absoluteFolderUrl: string = `${parsedUrl.origin}${parentFolderPath}`;
const relativeFolderPath = absoluteFolderUrl.replace(options.webUrl, '');

const parentFolders: string[] = relativeFolderPath.split('/').filter(folder => folder !== '');

for (let i = 1; i < parentFolders.length; i++) {
const currentFolderPath = parentFolders.slice(0, i + 1).join('/');

if (this.verbose) {
await logger.logToStderr(`Checking if folder '${currentFolderPath}' exists...`);
}

const folderExists = await this.getFolderExists(options.webUrl, currentFolderPath);
if (!folderExists) {
await this.addFolder(currentFolderPath, options, logger);
}
}
}

private async getFolderExists(webUrl: string, folderServerRelativeUrl: string): Promise<boolean> {
const requestUrl = `${webUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(folderServerRelativeUrl)}')?$select=Exists`;

const requestOptions: CliRequestOptions = {
url: requestUrl,
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};

const response = await request.get<{ Exists: boolean }>(requestOptions);
return response.Exists;
}

private async addFolder(serverRelativeUrl: string, options: Options, logger: Logger): Promise<FolderProperties> {
if (this.verbose) {
const folderName = serverRelativeUrl.split('/').pop();
const folderLocation = serverRelativeUrl.split('/').slice(0, -1).join('/');
await logger.logToStderr(`Adding folder with name '${folderName}' at location '${folderLocation}'...`);
}

const requestOptions: CliRequestOptions = {
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};

if (options.color === undefined) {
requestOptions.url = `${options.webUrl}/_api/web/folders/addUsingPath(decodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}')`;
}
else {
requestOptions.url = `${options.webUrl}/_api/foldercoloring/createfolder(DecodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl)}', overwrite=false)`;
requestOptions.data = {
coloringInformation: {
ColorHex: FolderColorValues[options.color] || options.color
}
};
}

const response = await request.post<FolderProperties>(requestOptions);
return response;
}
}

export default new SpoFolderAddCommand();

0 comments on commit 2b8cedf

Please sign in to comment.