From aeb0240ef6da156b14bc3a0b2d43f7e3d5791495 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Tue, 25 Jul 2023 12:23:21 +0200 Subject: [PATCH 1/3] Adds command `listitem attachment add` --- .../spo/listitem/listitem-attachment-add.mdx | 110 ++++++++++ docs/src/config/sidebars.js | 5 + src/m365/spo/commands.ts | 1 + .../listitem/listitem-attachment-add.spec.ts | 188 ++++++++++++++++++ .../listitem/listitem-attachment-add.ts | 156 +++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 docs/docs/cmd/spo/listitem/listitem-attachment-add.mdx create mode 100644 src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts create mode 100644 src/m365/spo/commands/listitem/listitem-attachment-add.ts diff --git a/docs/docs/cmd/spo/listitem/listitem-attachment-add.mdx b/docs/docs/cmd/spo/listitem/listitem-attachment-add.mdx new file mode 100644 index 00000000000..415842868d8 --- /dev/null +++ b/docs/docs/cmd/spo/listitem/listitem-attachment-add.mdx @@ -0,0 +1,110 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spo listitem attachment add + +Adds an attachment to a list item + +## Usage + +```sh +m365 spo listitem attachment add [options] +``` + +## Options + +```md definition-list +`-u, --webUrl ` +: URL of the site where the list item is located. + +`--listId [listId]` +: ID of the list. Specify either `listTitle`, `listId` or `listUrl`. + +`--listTitle [listTitle]` +: Title of the list. Specify either `listTitle`, `listId` or `listUrl`. + +`--listUrl [listUrl]` +: Server- or site-relative URL of the list. Specify either `listTitle`, `listId` or `listUrl`. + +`--listItemId ` +: The ID of the list item. + +`-p, --filePath ` +: Local path to the file that will be added as an attachment to the list item. + +`-n, --fileName [fileName]` +: Name for the file. If no name is provided, the name from `filePath` will be utilized. +``` + + + +## Examples + +Add a new attachment to a list item from a local file by using list title + +```sh +m365 spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle "DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" +``` + +Add a new attachment to a list item from a local file by using list URL with a different filename + +```sh +m365 spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listUrl "/sites/project-x/Lists/DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" --fileName "File2.jpg" +``` + +## Response + + + + + ```json + [ + { + "FileName": "File1.jpg", + "FileNameAsPath": { + "DecodedUrl": "File1.jpg" + }, + "ServerRelativePath": { + "DecodedUrl": "/Lists/DemoList/Attachments/147/File1.jpg" + }, + "ServerRelativeUrl": "/Lists/Test/Attachments/147/File1.jpg" + } + ] + ``` + + + + + ```text + FileName : File1.jpg + FileNameAsPath : {"DecodedUrl":"File1jpg"} + ServerRelativePath: {"DecodedUrl":"/Lists/DemoList/Attachments/743/File1.jpg"} + ServerRelativeUrl : /Lists/DemoList/Attachments/147/File1.jpg + ``` + + + + + ```csv + FileName,ServerRelativeUrl + File1.jpg,/Lists/DemoList/Attachments/147/File1.jpg + ``` + + + + + ```md + # spo listitem attachment add --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle "DemoList" --listItemId 147 --filePath "C:/Reports/File1.jpg" + + Date: 25/07/2023 + + Property | Value + ---------|------- + FileName | File1.jpg + ServerRelativeUrl | /Lists/DemoList/Attachments/147/File1.jpg + ``` + + + + diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 5183d89f4a5..7260e1e3fc4 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -2629,6 +2629,11 @@ const sidebars = { label: 'listitem remove', id: 'cmd/spo/listitem/listitem-remove' }, + { + type: 'doc', + label: 'listitem attachment add', + id: 'cmd/spo/listitem/listitem-attachment-add' + }, { type: 'doc', label: 'listitem attachment list', diff --git a/src/m365/spo/commands.ts b/src/m365/spo/commands.ts index de8605ad244..7a9ead59a71 100644 --- a/src/m365/spo/commands.ts +++ b/src/m365/spo/commands.ts @@ -157,6 +157,7 @@ export default { LIST_WEBHOOK_REMOVE: `${prefix} list webhook remove`, LIST_WEBHOOK_SET: `${prefix} list webhook set`, LISTITEM_ADD: `${prefix} listitem add`, + LISTITEM_ATTACHMENT_ADD: `${prefix} listitem attachment add`, LISTITEM_ATTACHMENT_LIST: `${prefix} listitem attachment list`, LISTITEM_BATCH_ADD: `${prefix} listitem batch add`, LISTITEM_BATCH_SET: `${prefix} listitem batch set`, diff --git a/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts b/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts new file mode 100644 index 00000000000..c0803cfc97d --- /dev/null +++ b/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts @@ -0,0 +1,188 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { telemetry } from '../../../../telemetry.js'; +import auth from '../../../../Auth.js'; +import { Cli } from '../../../../cli/Cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import { formatting } from '../../../../utils/formatting.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { urlUtil } from '../../../../utils/urlUtil.js'; +import commands from '../../commands.js'; +import fs from 'fs'; +import request from '../../../../request.js'; +import command from './listitem-attachment-add.js'; + +describe(commands.LISTITEM_ATTACHMENT_ADD, () => { + const webUrl = 'https://contoso.sharepoint.com/sites/project-x'; + const listId = '236a0f92482d475bba8fd0e4f78555e4'; + const listTitle = 'Test list'; + const listUrl = 'sites/project-x/lists/testlist'; + const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl); + const listItemId = 1; + const filePath = 'C:\\Temp\\Test.pdf'; + const fileName = 'CLIRocks.pdf'; + + const response = { 'FileName': 'CLIRocks.pdf', 'FileNameAsPath': { 'DecodedUrl': 'Testje.pdf' }, 'ServerRelativePath': { 'DecodedUrl': '/Lists/aaaaaa/Attachments/743/Testje.pdf' }, 'ServerRelativeUrl': '/Lists/aaaaaa/Attachments/743/Testje.pdf' }; + + let cli: Cli; + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + cli = Cli.getInstance(); + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake(((_, defaultValue) => defaultValue)); + }); + + afterEach(() => { + sinonUtil.restore([, + fs.existsSync, + fs.readFileSync, + request.post, + cli.getSettingWithDefaultValue + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.LISTITEM_ATTACHMENT_ADD); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = await command.validate({ options: { webUrl: 'invalid', listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if the webUrl option is a valid SharePoint site URL and filePath exists', async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = await command.validate({ options: { webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); + assert(actual); + }); + + it('fails validation if the listId option is not a valid GUID', async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = await command.validate({ options: { webUrl: webUrl, listId: 'invalid', listItemId: listItemId, filePath: filePath } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if the listId option is a valid GUID', async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath } }, commandInfo); + assert(actual); + }); + + it('fails validation if filePath does not exist', async () => { + sinon.stub(fs, 'existsSync').returns(false); + const actual = await command.validate({ options: { webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('adds attachment to listitem in list retrieved by id while specifying fileName', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns('content read'); + sinon.stub(request, 'post').callsFake(async (args) => { + if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { + return response; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }); + assert(loggerLogSpy.calledWith(response)); + }); + + it('adds attachment to listitem in list retrieved by url while not specifying fileName', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns('content read'); + sinon.stub(request, 'post').callsFake(async (args) => { + if (args.url === `${webUrl}/_api/web/GetList('${formatting.encodeQueryParameter(listServerRelativeUrl)}')/items(${listItemId})/AttachmentFiles/add(FileName='${filePath.replace(/^.*[\\\/]/, '')}')`) { + return response; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, webUrl: webUrl, listUrl: listUrl, listItemId: listItemId, filePath: filePath } }); + assert(loggerLogSpy.calledWith(response)); + }); + + it('adds attachment to listitem in list retrieved by url while specifying fileName without extension', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns('content read'); + const fileNameWithoutExtension = fileName.split('.')[0]; + const fileNameWithExtension = `${fileNameWithoutExtension}.${filePath.split('.').pop()}`; + sinon.stub(request, 'post').callsFake(async (args) => { + if (args.url === `${webUrl}/_api/web/lists/getByTitle('${formatting.encodeQueryParameter(listTitle)}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileNameWithExtension}')`) { + return response; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath, fileName: fileNameWithoutExtension } }); + assert(loggerLogSpy.calledWith(response)); + }); + + it('handles error when file with specific name already exists', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns('content read'); + const error = { + error: { + 'odata.error': { + code: '-2130575257, Microsoft.SharePoint.SPException', + message: { + lang: 'en-US', + value: 'The specified name is already in use.\n\nThe document or folder name was not changed. To change the name to a different value, close this dialog and edit the properties of the document or folder.' + } + } + } + }; + sinon.stub(request, 'post').callsFake(async (args) => { + if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }), + new CommandError(error.error['odata.error'].message.value)); + }); +}); diff --git a/src/m365/spo/commands/listitem/listitem-attachment-add.ts b/src/m365/spo/commands/listitem/listitem-attachment-add.ts new file mode 100644 index 00000000000..b711957eb11 --- /dev/null +++ b/src/m365/spo/commands/listitem/listitem-attachment-add.ts @@ -0,0 +1,156 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { formatting } from '../../../../utils/formatting.js'; +import { urlUtil } from '../../../../utils/urlUtil.js'; +import { validation } from '../../../../utils/validation.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import commands from '../../commands.js'; +import fs from 'fs'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + webUrl: string; + listId?: string; + listTitle?: string; + listUrl?: string; + listItemId: string; + fileName?: string; + filePath: string; +} + +class SpoListItemAttachmentAddCommand extends SpoCommand { + public get name(): string { + return commands.LISTITEM_ATTACHMENT_ADD; + } + + public get description(): string { + return 'Adds an attachment to a list item'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + listId: typeof args.options.listId !== 'undefined', + listTitle: typeof args.options.listTitle !== 'undefined', + listUrl: typeof args.options.listUrl !== 'undefined', + fileName: typeof args.options.fileName !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-u, --webUrl ' + }, + { + option: '--listId [listId]' + }, + { + option: '--listTitle [listTitle]' + }, + { + option: '--listUrl [listUrl]' + }, + { + option: '--listItemId ' + }, + { + option: '-p, --filePath ' + }, + { + option: '-n, --fileName [fileName]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + const isValidSharePointUrl: boolean | string = validation.isValidSharePointUrl(args.options.webUrl); + if (isValidSharePointUrl !== true) { + return isValidSharePointUrl; + } + + if (args.options.listId && !validation.isValidGuid(args.options.listId)) { + return `${args.options.listId} in option listId is not a valid GUID`; + } + + if (!fs.existsSync(args.options.filePath)) { + return `File with path '${args.options.filePath}' was not found.`; + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['listId', 'listTitle', 'listUrl'] }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Adding attachment to listitem with id ${args.options.listItemId} on list ${args.options.listId || args.options.listTitle || args.options.listUrl} on web ${args.options.webUrl}`); + } + + try { + const fileName = this.getFileName(args.options.filePath, args.options.fileName); + const fileBody: Buffer = fs.readFileSync(args.options.filePath); + const requestOptions: CliRequestOptions = { + url: `${args.options.webUrl}/_api/web/${this.getListUrl(args.options.webUrl, args.options.listId, args.options.listTitle, args.options.listUrl)}/items(${args.options.listItemId})/AttachmentFiles/add(FileName='${fileName}')`, + headers: { + 'accept': 'application/json;odata=nometadata' + }, + data: fileBody, + responseType: 'json' + }; + + const response = await request.post(requestOptions); + logger.log(response); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private getFileName(filePath: string, fileName?: string): string { + if (!fileName) { + return filePath.replace(/^.*[\\\/]/, ''); + } + + const extension = filePath.split('.').pop(); + if (!fileName.endsWith(`.${extension}`)) { + fileName += `.${extension}`; + } + return fileName; + } + + private getListUrl(webUrl: string, listId?: string, listTitle?: string, listUrl?: string): string { + if (listId) { + return `lists(guid'${formatting.encodeQueryParameter(listId)}')`; + } + else if (listTitle) { + return `lists/getByTitle('${formatting.encodeQueryParameter(listTitle)}')`; + } + else { + const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl!); + return `GetList('${formatting.encodeQueryParameter(listServerRelativeUrl)}')`; + } + } +} + +export default new SpoListItemAttachmentAddCommand(); From c1c95617d367e30dd51c86f2e927d710bd38a685 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Mon, 4 Sep 2023 11:04:42 +0200 Subject: [PATCH 2/3] Forgot an await --- src/m365/spo/commands/listitem/listitem-attachment-add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m365/spo/commands/listitem/listitem-attachment-add.ts b/src/m365/spo/commands/listitem/listitem-attachment-add.ts index b711957eb11..3bdc0722519 100644 --- a/src/m365/spo/commands/listitem/listitem-attachment-add.ts +++ b/src/m365/spo/commands/listitem/listitem-attachment-add.ts @@ -120,7 +120,7 @@ class SpoListItemAttachmentAddCommand extends SpoCommand { }; const response = await request.post(requestOptions); - logger.log(response); + await logger.log(response); } catch (err: any) { this.handleRejectedODataJsonPromise(err); From 789866fbec2bbc780b5e28e64c145262325bfe01 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Mon, 18 Sep 2023 12:37:20 +0200 Subject: [PATCH 3/3] Changes from Jasey --- .../listitem/listitem-attachment-add.spec.ts | 38 +++++++++++++++++-- .../listitem/listitem-attachment-add.ts | 19 ++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts b/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts index c0803cfc97d..5a7d75fcd32 100644 --- a/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts +++ b/src/m365/spo/commands/listitem/listitem-attachment-add.spec.ts @@ -101,6 +101,12 @@ describe(commands.LISTITEM_ATTACHMENT_ADD, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if the listItemId option is not a valid number', async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: 'invalid', filePath: filePath } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('passes validation if the listId option is a valid GUID', async () => { sinon.stub(fs, 'existsSync').returns(true); const actual = await command.validate({ options: { webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath } }, commandInfo); @@ -125,7 +131,7 @@ describe(commands.LISTITEM_ATTACHMENT_ADD, () => { }); await command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }); - assert(loggerLogSpy.calledWith(response)); + assert(loggerLogSpy.calledOnceWith(response)); }); it('adds attachment to listitem in list retrieved by url while not specifying fileName', async () => { @@ -140,7 +146,7 @@ describe(commands.LISTITEM_ATTACHMENT_ADD, () => { }); await command.action(logger, { options: { verbose: true, webUrl: webUrl, listUrl: listUrl, listItemId: listItemId, filePath: filePath } }); - assert(loggerLogSpy.calledWith(response)); + assert(loggerLogSpy.calledOnceWith(response)); }); it('adds attachment to listitem in list retrieved by url while specifying fileName without extension', async () => { @@ -157,7 +163,7 @@ describe(commands.LISTITEM_ATTACHMENT_ADD, () => { }); await command.action(logger, { options: { verbose: true, webUrl: webUrl, listTitle: listTitle, listItemId: listItemId, filePath: filePath, fileName: fileNameWithoutExtension } }); - assert(loggerLogSpy.calledWith(response)); + assert(loggerLogSpy.calledOnceWith(response)); }); it('handles error when file with specific name already exists', async () => { @@ -182,6 +188,32 @@ describe(commands.LISTITEM_ATTACHMENT_ADD, () => { throw 'Invalid request'; }); + await assert.rejects(command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }), + new CommandError(error.error['odata.error'].message.value.split('\n')[0])); + }); + + it('handles API error', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns('content read'); + const error = { + error: { + 'odata.error': { + code: '-2130575257, Microsoft.SharePoint.SPException', + message: { + lang: 'en-US', + value: 'An error has occured.' + } + } + } + }; + sinon.stub(request, 'post').callsFake(async (args) => { + if (args.url === `${webUrl}/_api/web/lists(guid'${listId}')/items(${listItemId})/AttachmentFiles/add(FileName='${fileName}')`) { + throw error; + } + + throw 'Invalid request'; + }); + await assert.rejects(command.action(logger, { options: { verbose: true, webUrl: webUrl, listId: listId, listItemId: listItemId, filePath: filePath, fileName: fileName } }), new CommandError(error.error['odata.error'].message.value)); }); diff --git a/src/m365/spo/commands/listitem/listitem-attachment-add.ts b/src/m365/spo/commands/listitem/listitem-attachment-add.ts index 3bdc0722519..d1f340374a5 100644 --- a/src/m365/spo/commands/listitem/listitem-attachment-add.ts +++ b/src/m365/spo/commands/listitem/listitem-attachment-add.ts @@ -17,7 +17,7 @@ interface Options extends GlobalOptions { listId?: string; listTitle?: string; listUrl?: string; - listItemId: string; + listItemId: number; fileName?: string; filePath: string; } @@ -85,8 +85,12 @@ class SpoListItemAttachmentAddCommand extends SpoCommand { return isValidSharePointUrl; } + if (isNaN(args.options.listItemId)) { + return `${args.options.listItemId} in option listItemId is not a valid number.`; + } + if (args.options.listId && !validation.isValidGuid(args.options.listId)) { - return `${args.options.listId} in option listId is not a valid GUID`; + return `${args.options.listId} in option listId is not a valid GUID.`; } if (!fs.existsSync(args.options.filePath)) { @@ -104,7 +108,7 @@ class SpoListItemAttachmentAddCommand extends SpoCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { - await logger.logToStderr(`Adding attachment to listitem with id ${args.options.listItemId} on list ${args.options.listId || args.options.listTitle || args.options.listUrl} on web ${args.options.webUrl}`); + await logger.logToStderr(`Adding an attachment to list item with id ${args.options.listItemId} on list ${args.options.listId || args.options.listTitle || args.options.listUrl} on web ${args.options.webUrl}.`); } try { @@ -123,7 +127,14 @@ class SpoListItemAttachmentAddCommand extends SpoCommand { await logger.log(response); } catch (err: any) { - this.handleRejectedODataJsonPromise(err); + if (err.error && + err.error['odata.error'] && + err.error['odata.error'].message && err.error['odata.error'].message.value.indexOf('The document or folder name was not changed.') > -1) { + this.handleError(err.error['odata.error'].message.value.split('\n')[0]); + } + else { + this.handleRejectedODataJsonPromise(err); + } } }