From 9b28b0af30c3cd0fb534f1106dcb44463c9976e1 Mon Sep 17 00:00:00 2001
From: Milan Holemans <11723921+milanholemans@users.noreply.github.com>
Date: Fri, 23 Aug 2024 00:23:51 +0200
Subject: [PATCH] Reworks 'spo file move' command to new endpoint. Closes #6153
---
docs/docs/cmd/spo/file/file-move.mdx | 125 ++++-
docs/docs/v10-upgrade-guidance.mdx | 18 +
src/m365/spo/commands/file/file-copy.spec.ts | 4 +
src/m365/spo/commands/file/file-copy.ts | 7 +-
src/m365/spo/commands/file/file-move.spec.ts | 534 ++++++++++++++-----
src/m365/spo/commands/file/file-move.ts | 105 ++--
src/utils/spo.spec.ts | 48 +-
src/utils/spo.ts | 10 +-
8 files changed, 668 insertions(+), 183 deletions(-)
diff --git a/docs/docs/cmd/spo/file/file-move.mdx b/docs/docs/cmd/spo/file/file-move.mdx
index c25706ef3bf..a3470fa73bf 100644
--- a/docs/docs/cmd/spo/file/file-move.mdx
+++ b/docs/docs/cmd/spo/file/file-move.mdx
@@ -1,4 +1,6 @@
import Global from '/docs/cmd/_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
# spo file move
@@ -31,11 +33,14 @@ m365 spo file move [options]
`--nameConflictBehavior [nameConflictBehavior]`
: Behavior when a file or folder with the same name is already present at the destination. Allowed values: `fail`, `replace`, `rename`. Defaults to `fail`.
-`--retainEditorAndModified`
-: Use this option to retain the editor and modified date. When not specified, these values are reset when moving to another library.
+`--includeItemPermissions`
+: Ensure that item-level permissions are preserved during the move.
`--bypassSharedLock`
: This indicates whether a file with a share lock can still be moved. Use this option to move a file that is locked.
+
+`--skipWait`
+: Don't wait for the copy operation to complete.
```
@@ -45,7 +50,6 @@ m365 spo file move [options]
All file versions are retained while moving a file.
When you specify a value for `nameConflictBehavior`, consider the following:
-
- `fail` will throw an error when the destination file already exists.
- `replace` will replace the destination file if it already exists.
- `rename` will add a suffix (e.g. Document1.pdf) when the destination file already exists.
@@ -70,16 +74,119 @@ Move a file to another document library and replace a file with the same name.
m365 spo file move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceUrl "/Shared Documents/Report project-x.pdf" --targetUrl "/sites/project-x/My Documents" --newName Report.pdf --nameConflictBehavior replace
```
-Move a file referenced by its ID to another document library and retain editor and modified date.
+Move a file referenced by its ID to another document library and retain item-level permissions.
```sh
-m365 spo file move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceId b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2 --targetUrl "/sites/project-x/My Documents" --retainEditorAndModified
+m365 spo file move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceId b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2 --targetUrl "/sites/project-x/My Documents" --includeItemPermissions
```
## Response
-The command won't return a response on success.
+### Standard Response
+
+
+
+
+ ```json
+ {
+ "CheckInComment": "",
+ "CheckOutType": 2,
+ "ContentTag": "{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1,1",
+ "CustomizedPageStatus": 0,
+ "ETag": "\"{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1\"",
+ "Exists": true,
+ "IrmEnabled": false,
+ "Length": "5987",
+ "Level": 1,
+ "LinkingUri": null,
+ "LinkingUrl": "",
+ "MajorVersion": 1,
+ "MinorVersion": 0,
+ "Name": "Test1.docx",
+ "ServerRelativeUrl": "/sites/project-x/documents/Test1.docx",
+ "TimeCreated": "2022-10-30T10:16:18Z",
+ "TimeLastModified": "2022-10-30T10:16:18Z",
+ "Title": null,
+ "UIVersion": 512,
+ "UIVersionLabel": "1.0",
+ "UniqueId": "b2307a39-e878-458b-bc90-03bc578531d6"
+ }
+ ```
+
+
+
+
+ ```text
+ CheckInComment :
+ CheckOutType : 2
+ ContentTag : {03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1,1
+ CustomizedPageStatus: 0
+ ETag : "{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1"
+ Exists : true
+ IrmEnabled : false
+ Length : 5987
+ Level : 1
+ LinkingUri : null
+ LinkingUrl :
+ MajorVersion : 1
+ MinorVersion : 0
+ Name : Test1.docx
+ ServerRelativeUrl : /sites/project-x/documents/Test1.docx
+ TimeCreated : 2022-10-30T10:16:18Z
+ TimeLastModified : 2022-10-30T10:16:18Z
+ Title : null
+ UIVersion : 512
+ UIVersionLabel : 1.0
+ UniqueId : b2307a39-e878-458b-bc90-03bc578531d6
+ ```
+
+
+
+
+ ```csv
+ CheckInComment,CheckOutType,ContentTag,CustomizedPageStatus,ETag,Exists,IrmEnabled,Length,Level,LinkingUri,LinkingUrl,MajorVersion,MinorVersion,Name,ServerRelativeUrl,TimeCreated,TimeLastModified,Title,UIVersion,UIVersionLabel,UniqueId
+ ,2,"{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1,1",0,"""{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1""",1,,5987,1,,,1,0,Test1.docx,/sites/project-x/documents/Test1.docx,2022-10-30T10:16:18Z,2022-10-30T10:16:18Z,,512,1.0,b2307a39-e878-458b-bc90-03bc578531d6
+ ```
+
+
+
+
+ ```md
+ # spo file move --webUrl "https://contoso.sharepoint.com/sites/IT" --sourceUrl "/Shared Documents/Document.docx" --targetUrl "/sites/project-x/Shared Documents"
+
+ Date: 10/3/2023
+
+ ## b2307a39-e878-458b-bc90-03bc578531d6
+
+ Property | Value
+ ---------|-------
+ CheckInComment |
+ CheckOutType | 2
+ ContentTag | {03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1,1
+ CustomizedPageStatus | 0
+ ETag | "{03E171B6-9DF2-46D9-A7A1-D5FA0BD23E4B},1"
+ Exists | true
+ ExistsAllowThrowForPolicyFailures | true
+ IrmEnabled | false
+ Length | 5987
+ Level | 1
+ LinkingUri | https://contoso.sharepoint.com/sites/project-x/shared%20documents/Document.docx?d=w59d4e6fcf6f94ce78bea0273cedb1a19
+ LinkingUrl | https://contoso.sharepoint.com/sites/project-x/shared documents/Document.docx?d=w59d4e6fcf6f94ce78bea0273cedb1a19
+ MajorVersion | 1
+ MinorVersion | 0
+ Name | Document.docx
+ ServerRelativeUrl | /sites/project-x/shared documents/Document.docx
+ TimeCreated | 2023-05-23T09:51:34Z
+ TimeLastModified | 2023-05-23T10:13:01Z
+ Title |
+ UIVersion | 1536
+ UIVersionLabel | 1.0
+ UniqueId | b2307a39-e878-458b-bc90-03bc578531d6
+ ```
+
+
+
+
+### `skipWait` response
-## More information
-
-- Move items from a SharePoint document library: [https://support.office.com/en-us/article/move-or-copy-items-from-a-sharepoint-document-library-00e2f483-4df3-46be-a861-1f5f0c1a87bc](https://support.office.com/en-us/article/move-or-copy-items-from-a-sharepoint-document-library-00e2f483-4df3-46be-a861-1f5f0c1a87bc)
+The command won't return a response on success.
diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx
index a68006e94d1..75788bd99de 100644
--- a/docs/docs/v10-upgrade-guidance.mdx
+++ b/docs/docs/v10-upgrade-guidance.mdx
@@ -159,6 +159,24 @@ In the past versions of CLI for Microsoft 365, the command had no output. When u
When using the [spo file copy](./cmd/spo/file/file-copy.mdx) command, please use the new command input. This means that you'll have to remove option `--resetAuthorAndCreated` from your scripts and automation tools.
+### Updated command `spo file move`
+
+Just like `spo file copy`, we have decided to move the [spo file move](./cmd/spo/file/file-move.mdx) command to a new endpoint. This change is necessary to ensure the command's functionality and reliability. Because of the new endpoint, the command input and output have changed.
+
+**Command options:**
+
+Unfortunately, we had to drop the `--retainEditorAndModified` options as it's no longer supported by the new endpoint. In return, we were able to add two new options:
+- `--includeItemPermissions`: Ensure that item-level permissions are preserved during the move.
+- `--skipWait`: Don't wait for the copy operation to complete.
+
+**Command output:**
+
+In the past versions of CLI for Microsoft 365, the command had no output. When using option `--nameConflictBehavior rename`, it's hard for the user to know what the actual name of the copied file is. With the new endpoint, the command now returns the file information about the destination file, providing you with all the info you need to execute other commands on this file.
+
+#### What action do I need to take?
+
+When using the [spo file move](./cmd/spo/file/file-move.mdx) command, please use the new command input. This means that you'll have to remove option `--retainEditorAndModified` from your scripts and automation tools.
+
### Removed `spo folder rename` alias
The `spo folder rename` command was removed and replaced by the [spo folder set](./cmd/spo/folder/folder-set.mdx) command.
diff --git a/src/m365/spo/commands/file/file-copy.spec.ts b/src/m365/spo/commands/file/file-copy.spec.ts
index 55cc6f2907e..c9b10a0c4b6 100644
--- a/src/m365/spo/commands/file/file-copy.spec.ts
+++ b/src/m365/spo/commands/file/file-copy.spec.ts
@@ -303,6 +303,7 @@ describe(commands.FILE_COPY, () => {
nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail,
bypassSharedLock: false,
ignoreVersionHistory: false,
+ operation: 'copy',
newName: undefined
}
]);
@@ -334,6 +335,7 @@ describe(commands.FILE_COPY, () => {
nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail,
bypassSharedLock: false,
ignoreVersionHistory: false,
+ operation: 'copy',
newName: undefined
}
]);
@@ -368,6 +370,7 @@ describe(commands.FILE_COPY, () => {
nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename,
bypassSharedLock: true,
ignoreVersionHistory: true,
+ operation: 'copy',
newName: 'Document-renamed.pdf'
}
]);
@@ -400,6 +403,7 @@ describe(commands.FILE_COPY, () => {
nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace,
bypassSharedLock: false,
ignoreVersionHistory: false,
+ operation: 'copy',
newName: 'Document-renamed.pdf'
}
]);
diff --git a/src/m365/spo/commands/file/file-copy.ts b/src/m365/spo/commands/file/file-copy.ts
index 757ca8232c1..ca73f20b733 100644
--- a/src/m365/spo/commands/file/file-copy.ts
+++ b/src/m365/spo/commands/file/file-copy.ts
@@ -120,6 +120,10 @@ class SpoFileCopyCommand extends SpoCommand {
this.types.boolean.push('bypassSharedLock', 'ignoreVersionHistory', 'skipWait');
}
+ protected getExcludedOptionsWithUrls(): string[] | undefined {
+ return ['targetUrl', 'sourceUrl'];
+ }
+
public async commandAction(logger: Logger, args: CommandArgs): Promise {
try {
const sourceServerRelativePath = await this.getSourcePath(logger, args.options);
@@ -144,7 +148,8 @@ class SpoFileCopyCommand extends SpoCommand {
nameConflictBehavior: this.getNameConflictBehaviorValue(args.options.nameConflictBehavior),
bypassSharedLock: !!args.options.bypassSharedLock,
ignoreVersionHistory: !!args.options.ignoreVersionHistory,
- newName: newName
+ newName: newName,
+ operation: 'copy'
}
);
diff --git a/src/m365/spo/commands/file/file-move.spec.ts b/src/m365/spo/commands/file/file-move.spec.ts
index e4d2eb7b2c1..6a8bb6be373 100644
--- a/src/m365/spo/commands/file/file-move.spec.ts
+++ b/src/m365/spo/commands/file/file-move.spec.ts
@@ -12,21 +12,79 @@ import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './file-move.js';
+import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js';
+import { settingsNames } from '../../../../settingsNames.js';
describe(commands.FILE_MOVE, () => {
- const fileName = 'Report.docx';
- const rootUrl = 'https://contoso.sharepoint.com';
- const webUrl = rootUrl + '/sites/project-x';
- const sourceUrl = '/sites/project-x/documents/' + fileName;
- const targetUrl = '/sites/project-y/My Documents';
- const absoluteSourceUrl = rootUrl + sourceUrl;
- const absoluteTargetUrl = rootUrl + targetUrl;
- const sourceId = 'b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2';
+ const sourceWebUrl = 'https://contoso.sharepoint.com/sites/Sales';
+ const sourceDocumentName = 'Document.pdf';
+ const sourceServerRelUrl = '/sites/Sales/Shared Documents/' + sourceDocumentName;
+ const sourceSiteRelUrl = '/Shared Documents/' + sourceDocumentName;
+ const sourceAbsoluteUrl = 'https://contoso.sharepoint.com' + sourceServerRelUrl;
+ const sourceDocId = 'f09c4efe-b8c0-4e89-a166-03418661b89b';
+
+ const destWebUrl = 'https://contoso.sharepoint.com/sites/Marketing';
+ const destSiteRelUrl = '/Documents/Logos';
+ const destServerRelUrl = '/sites/Marketing' + destSiteRelUrl;
+ const destAbsoluteTargetUrl = 'https://contoso.sharepoint.com' + destServerRelUrl;
+ const destDocId = '15488d89-b82b-40be-958a-922b2ed79383';
+
+ const copyJobInfo = {
+ EncryptionKey: '2by8+2oizihYOFqk02Tlokj8lWUShePAEE+WMuA9lzA=',
+ JobId: 'd812e5a0-d95a-4e4f-bcb7-d4415e88c8ee',
+ JobQueueUri: 'https://spoam1db1m020p4.queue.core.windows.net/2-1499-20240831-29533e6c72c6464780b756c71ea3fe92?sv=2018-03-28&sig=aX%2BNOkUimZ3f%2B%2BvdXI95%2FKJI1e5UE6TU703Dw3Eb5c8%3D&st=2024-08-09T00%3A00%3A00Z&se=2024-08-31T00%3A00%3A00Z&sp=rap',
+ SourceListItemUniqueIds: [
+ sourceDocId
+ ]
+ };
+
+ const copyJobResult = {
+ Event: 'JobFinishedObjectInfo',
+ JobId: '6d1eda82-0d1c-41eb-ab05-1d9cd4afe786',
+ Time: '08/10/2024 18:59:40.145',
+ SourceObjectFullUrl: sourceAbsoluteUrl,
+ TargetServerUrl: 'https://contoso.sharepoint.com',
+ TargetSiteId: '794dada8-4389-45ce-9559-0de74bf3554a',
+ TargetWebId: '8de9b4d3-3c30-4fd0-a9d7-2452bd065555',
+ TargetListId: '44b336a5-e397-4e22-a270-c39e9069b123',
+ TargetObjectUniqueId: destDocId,
+ TargetObjectSiteRelativeUrl: destSiteRelUrl.substring(1),
+ CorrelationId: '5efd44a1-c034-9000-9692-4e1a1b3ca33b'
+ };
+
+ const destFileResponse = {
+ CheckInComment: '',
+ CheckOutType: 2,
+ ContentTag: '{C194762B-3F54-4F5F-9F5C-EBA26084E29D},53,23',
+ CustomizedPageStatus: 0,
+ ETag: '"{C194762B-3F54-4F5F-9F5C-EBA26084E29D},53"',
+ Exists: true,
+ ExistsAllowThrowForPolicyFailures: true,
+ ExistsWithException: true,
+ IrmEnabled: false,
+ Length: '18911',
+ Level: 1,
+ LinkingUri: `${destAbsoluteTargetUrl + '/' + sourceDocumentName}?d=wc194762b3f544f5f9f5ceba26084e29d`,
+ LinkingUrl: '',
+ MajorVersion: 14,
+ MinorVersion: 0,
+ Name: sourceDocumentName,
+ ServerRelativeUrl: destServerRelUrl + '/' + sourceDocumentName,
+ TimeCreated: '2024-05-01T19:54:50Z',
+ TimeLastModified: '2024-08-10T19:31:34Z',
+ Title: '',
+ UIVersion: 7168,
+ UIVersionLabel: '14.0',
+ UniqueId: destDocId
+ };
let log: any[];
let logger: Logger;
let commandInfo: CommandInfo;
- let postStub: sinon.SinonStub;
+ let loggerLogSpy: sinon.SinonSpy;
+
+ let spoUtilCreateCopyJobStub: sinon.SinonStub;
+ let spoUtilGetCopyJobResultStub: sinon.SinonStub;
before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
@@ -36,6 +94,8 @@ describe(commands.FILE_MOVE, () => {
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
+ sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => settingName === settingsNames.prompt ? false : defaultValue);
+ spoUtilCreateCopyJobStub = sinon.stub(spo, 'createCopyJob').resolves(copyJobInfo);
});
beforeEach(() => {
@@ -52,21 +112,15 @@ describe(commands.FILE_MOVE, () => {
}
};
- postStub = sinon.stub(request, 'post').callsFake(async opts => {
- if (opts.url === `${webUrl}/_api/SP.MoveCopyUtil.MoveFileByPath`) {
- return {
- 'odata.null': true
- };
- }
-
- throw 'Invalid request: ' + opts.url;
- });
+ loggerLogSpy = sinon.spy(logger, 'log');
+ spoUtilGetCopyJobResultStub = sinon.stub(spo, 'getCopyJobResult').resolves(copyJobResult);
});
afterEach(() => {
sinonUtil.restore([
request.post,
- request.get
+ request.get,
+ spo.getCopyJobResult
]);
});
@@ -83,209 +137,419 @@ describe(commands.FILE_MOVE, () => {
assert.notStrictEqual(command.description, null);
});
- it('excludes options from URL processing', () => {
- assert.deepStrictEqual((command as any).getExcludedOptionsWithUrls(), ['targetUrl', 'sourceUrl']);
- });
-
it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => {
- const actual = await command.validate({ options: { webUrl: 'foo', sourceUrl: sourceUrl, targetUrl: targetUrl } }, commandInfo);
+ const actual = await command.validate({ options: { webUrl: 'foo', sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl } }, commandInfo);
assert.notStrictEqual(actual, true);
});
it('fails validation if sourceId is not a valid guid', async () => {
- const actual = await command.validate({ options: { webUrl: webUrl, sourceId: 'invalid', targetUrl: targetUrl } }, commandInfo);
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceId: 'invalid', targetUrl: destServerRelUrl } }, commandInfo);
assert.notStrictEqual(actual, true);
});
it('fails validation if nameConflictBehavior is not valid', async () => {
- const actual = await command.validate({ options: { webUrl: webUrl, sourceUrl: sourceUrl, targetUrl: targetUrl, nameConflictBehavior: 'invalid' } }, commandInfo);
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl, nameConflictBehavior: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
});
it('passes validation if the sourceId is a valid GUID', async () => {
- const actual = await command.validate({ options: { webUrl: webUrl, sourceId: sourceId, targetUrl: targetUrl } }, commandInfo);
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceId: sourceDocId, targetUrl: destServerRelUrl } }, commandInfo);
assert.strictEqual(actual, true);
});
it('passes validation if the webUrl option is a valid SharePoint site URL', async () => {
- const actual = await command.validate({ options: { webUrl: webUrl, sourceUrl: sourceUrl, targetUrl: targetUrl } }, commandInfo);
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation if site-relative sourceUrl is provided', async () => {
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceSiteRelUrl, targetUrl: destServerRelUrl } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation if the sourceUrl is an absolute URL', async () => {
+ const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceAbsoluteUrl, targetUrl: destAbsoluteTargetUrl } }, commandInfo);
assert.strictEqual(actual, true);
});
- it('moves a file correctly when specifying sourceId', async () => {
- sinon.stub(request, 'get').callsFake(async opts => {
- if (opts.url === `${webUrl}/_api/Web/GetFileById('${sourceId}')?$select=ServerRelativePath`) {
+ it('correctly outputs exactly one result when file is moved when using sourceId', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${sourceWebUrl}/_api/Web/GetFileById('${sourceDocId}')/ServerRelativePath`) {
return {
- ServerRelativePath: {
- DecodedUrl: sourceUrl
- }
+ DecodedUrl: destAbsoluteTargetUrl + `/${sourceDocumentName}`
};
}
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
throw 'Invalid request: ' + opts.url;
});
await command.action(logger, {
options: {
- webUrl: webUrl,
- sourceId: sourceId,
- targetUrl: targetUrl,
- verbose: true
+ verbose: true,
+ webUrl: sourceWebUrl,
+ sourceId: sourceDocId,
+ targetUrl: destAbsoluteTargetUrl
}
});
- assert.deepStrictEqual(postStub.lastCall.args[0].data,
+ assert(loggerLogSpy.calledOnce);
+ });
+
+ it('correctly outputs result when file is moved when using sourceId', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${sourceWebUrl}/_api/Web/GetFileById('${sourceDocId}')/ServerRelativePath`) {
+ return {
+ DecodedUrl: sourceAbsoluteUrl
+ };
+ }
+
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ verbose: true,
+ webUrl: sourceWebUrl,
+ sourceId: sourceDocId,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ });
+
+ assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], destFileResponse);
+ });
+
+ it('correctly outputs exactly one result when file is moved when using sourceUrl', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ verbose: true,
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ });
+
+ assert(loggerLogSpy.calledOnce);
+ });
+
+ it('correctly outputs result when file is moved when using sourceUrl', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ verbose: true,
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ });
+
+ assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], destFileResponse);
+ });
+
+ it('correctly moves a file when using sourceId', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${sourceWebUrl}/_api/Web/GetFileById('${sourceDocId}')/ServerRelativePath`) {
+ return {
+ DecodedUrl: sourceAbsoluteUrl
+ };
+ }
+
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceId: sourceDocId,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ });
+
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
{
- srcPath: {
- DecodedUrl: absoluteSourceUrl
- },
- destPath: {
- DecodedUrl: absoluteTargetUrl + `/${fileName}`
- },
- overwrite: false,
- options: {
- KeepBoth: false,
- ShouldBypassSharedLocks: false,
- RetainEditorAndModifiedOnMove: false
- }
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail,
+ bypassSharedLock: false,
+ includeItemPermissions: false,
+ operation: 'move',
+ newName: undefined
}
- );
+ ]);
});
- it('moves a file correctly when specifying sourceUrl with server relative paths', async () => {
+ it('correctly moves a file when using sourceUrl', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
await command.action(logger, {
options: {
- webUrl: webUrl,
- sourceUrl: sourceUrl,
- targetUrl: targetUrl
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
+ nameConflictBehavior: 'fail'
}
});
- assert.deepStrictEqual(postStub.lastCall.args[0].data,
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
{
- srcPath: {
- DecodedUrl: absoluteSourceUrl
- },
- destPath: {
- DecodedUrl: absoluteTargetUrl + `/${fileName}`
- },
- overwrite: false,
- options: {
- KeepBoth: false,
- ShouldBypassSharedLocks: false,
- RetainEditorAndModifiedOnMove: false
- }
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail,
+ bypassSharedLock: false,
+ includeItemPermissions: false,
+ operation: 'move',
+ newName: undefined
}
- );
+ ]);
});
- it('moves a file correctly when specifying sourceUrl with site relative paths', async () => {
+ it('correctly moves a file when using site-relative sourceUrl', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
await command.action(logger, {
options: {
- webUrl: webUrl,
- sourceUrl: `/Shared Documents/${fileName}`,
- targetUrl: targetUrl,
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceSiteRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
nameConflictBehavior: 'fail'
}
});
- assert.deepStrictEqual(postStub.lastCall.args[0].data,
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
{
- srcPath: {
- DecodedUrl: webUrl + `/Shared Documents/${fileName}`
- },
- destPath: {
- DecodedUrl: absoluteTargetUrl + `/${fileName}`
- },
- overwrite: false,
- options: {
- KeepBoth: false,
- ShouldBypassSharedLocks: false,
- RetainEditorAndModifiedOnMove: false
- }
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail,
+ bypassSharedLock: false,
+ includeItemPermissions: false,
+ operation: 'move',
+ newName: undefined
}
- );
+ ]);
});
- it('moves a file correctly when specifying sourceUrl with absolute paths', async () => {
+ it('correctly moves a file when using absolute urls', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
await command.action(logger, {
options: {
- webUrl: webUrl,
- sourceUrl: rootUrl + sourceUrl,
- targetUrl: rootUrl + targetUrl,
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceAbsoluteUrl,
+ targetUrl: destAbsoluteTargetUrl,
nameConflictBehavior: 'replace'
}
});
- assert.deepStrictEqual(postStub.lastCall.args[0].data,
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
{
- srcPath: {
- DecodedUrl: absoluteSourceUrl
- },
- destPath: {
- DecodedUrl: absoluteTargetUrl + `/${fileName}`
- },
- overwrite: true,
- options: {
- KeepBoth: false,
- ShouldBypassSharedLocks: false,
- RetainEditorAndModifiedOnMove: false
- }
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace,
+ bypassSharedLock: false,
+ includeItemPermissions: false,
+ operation: 'move',
+ newName: undefined
}
- );
+ ]);
});
- it('moves a file correctly when specifying various options', async () => {
+ it('correctly moves a file when using sourceUrl with extra options', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
await command.action(logger, {
options: {
- webUrl: webUrl,
- sourceUrl: sourceUrl,
- targetUrl: targetUrl,
- newName: 'Report-old.docx',
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
nameConflictBehavior: 'rename',
- retainEditorAndModified: true,
- bypassSharedLock: true
+ bypassSharedLock: true,
+ includeItemPermissions: true,
+ newName: 'Document-renamed.pdf'
}
});
- assert.deepStrictEqual(postStub.lastCall.args[0].data,
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
{
- srcPath: {
- DecodedUrl: absoluteSourceUrl
- },
- destPath: {
- DecodedUrl: absoluteTargetUrl + '/Report-old.docx'
- },
- overwrite: false,
- options: {
- KeepBoth: true,
- ShouldBypassSharedLocks: true,
- RetainEditorAndModifiedOnMove: true
- }
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename,
+ bypassSharedLock: true,
+ includeItemPermissions: true,
+ operation: 'move',
+ newName: 'Document-renamed.pdf'
}
- );
+ ]);
});
- it('handles error correctly when moving a file', async () => {
- const error = {
+ it('correctly moves a file when using sourceUrl with new name without extension', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
+ nameConflictBehavior: 'replace',
+ newName: 'Document-renamed'
+ }
+ });
+
+ assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [
+ sourceWebUrl,
+ sourceAbsoluteUrl,
+ destAbsoluteTargetUrl,
+ {
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace,
+ bypassSharedLock: false,
+ includeItemPermissions: false,
+ operation: 'move',
+ newName: 'Document-renamed.pdf'
+ }
+ ]);
+ });
+
+ it('correctly polls for the copy job to finish', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `${destWebUrl}/_api/Web/GetFileById('${destDocId}')`) {
+ return destFileResponse;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ });
+
+ assert.deepStrictEqual(spoUtilGetCopyJobResultStub.lastCall.args, [
+ sourceWebUrl,
+ copyJobInfo
+ ]);
+ });
+
+ it('outputs no result when skipWait is specified', async () => {
+ await command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
+ skipWait: true
+ }
+ });
+
+ assert(loggerLogSpy.notCalled);
+ });
+
+ it('correctly skips polling when skipWait is specified', async () => {
+ await command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl,
+ skipWait: true
+ }
+ });
+
+ assert(spoUtilGetCopyJobResultStub.notCalled);
+ });
+
+ it('correctly handles error when sourceId does not exist', async () => {
+ sinon.stub(request, 'get').rejects({
error: {
'odata.error': {
+ code: '-2147024894, System.IO.FileNotFoundException',
message: {
lang: 'en-US',
value: 'File Not Found.'
}
}
}
- };
+ });
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ webUrl: sourceWebUrl,
+ sourceId: sourceDocId,
+ targetUrl: destAbsoluteTargetUrl
+ }
+ }), new CommandError('File Not Found.'));
+ });
- sinon.stub(request, 'get').rejects(error);
+ it('correctly handles error when getCopyJobResult fails', async () => {
+ spoUtilGetCopyJobResultStub.restore();
+ spoUtilGetCopyJobResultStub = sinon.stub(spo, 'getCopyJobResult').rejects(new Error('Target file already exists.'));
await assert.rejects(command.action(logger, {
options: {
- webUrl: webUrl,
- sourceId: sourceId,
- targetUrl: targetUrl
+ webUrl: sourceWebUrl,
+ sourceUrl: sourceServerRelUrl,
+ targetUrl: destAbsoluteTargetUrl
}
- }), new CommandError(error.error['odata.error'].message.value));
+ }), new CommandError('Target file already exists.'));
});
});
diff --git a/src/m365/spo/commands/file/file-move.ts b/src/m365/spo/commands/file/file-move.ts
index 8dbdebdbcc8..ae001e31ad9 100644
--- a/src/m365/spo/commands/file/file-move.ts
+++ b/src/m365/spo/commands/file/file-move.ts
@@ -1,6 +1,7 @@
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
+import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import { validation } from '../../../../utils/validation.js';
import SpoCommand from '../../../base/SpoCommand.js';
@@ -17,8 +18,9 @@ interface Options extends GlobalOptions {
targetUrl: string;
newName?: string;
nameConflictBehavior?: string;
- retainEditorAndModified?: boolean;
+ includeItemPermissions?: boolean;
bypassSharedLock?: boolean;
+ skipWait?: boolean;
}
class SpoFileMoveCommand extends SpoCommand {
@@ -49,8 +51,9 @@ class SpoFileMoveCommand extends SpoCommand {
sourceId: typeof args.options.sourceId !== 'undefined',
newName: typeof args.options.newName !== 'undefined',
nameConflictBehavior: typeof args.options.nameConflictBehavior !== 'undefined',
- retainEditorAndModified: !!args.options.retainEditorAndModified,
- bypassSharedLock: !!args.options.bypassSharedLock
+ includeItemPermissions: !!args.options.includeItemPermissions,
+ bypassSharedLock: !!args.options.bypassSharedLock,
+ skipWait: !!args.options.skipWait
});
});
}
@@ -77,10 +80,13 @@ class SpoFileMoveCommand extends SpoCommand {
autocomplete: this.nameConflictBehaviorOptions
},
{
- option: '--retainEditorAndModified'
+ option: '--includeItemPermissions'
},
{
option: '--bypassSharedLock'
+ },
+ {
+ option: '--skipWait'
}
);
}
@@ -112,7 +118,7 @@ class SpoFileMoveCommand extends SpoCommand {
#initTypes(): void {
this.types.string.push('webUrl', 'sourceUrl', 'sourceId', 'targetUrl', 'newName', 'nameConflictBehavior');
- this.types.boolean.push('retainEditorAndModified', 'bypassSharedLock');
+ this.types.boolean.push('includeItemPermissions', 'bypassSharedLock', 'skipWait');
}
protected getExcludedOptionsWithUrls(): string[] | undefined {
@@ -121,46 +127,61 @@ class SpoFileMoveCommand extends SpoCommand {
public async commandAction(logger: Logger, args: CommandArgs): Promise {
try {
- const sourcePath = await this.getSourcePath(logger, args.options);
+ const sourceServerRelativePath = await this.getSourcePath(logger, args.options);
+ const sourcePath = this.getAbsoluteUrl(args.options.webUrl, sourceServerRelativePath);
+ const destinationPath = this.getAbsoluteUrl(args.options.webUrl, args.options.targetUrl);
if (this.verbose) {
- await logger.logToStderr(`Moving file ${sourcePath} to ${args.options.targetUrl}...`);
+ await logger.logToStderr(`Moving file '${sourceServerRelativePath}' to '${args.options.targetUrl}'...`);
+ }
+
+ let newName = args.options.newName;
+ // Add original file extension if not provided
+ if (newName && !newName.includes('.')) {
+ newName += sourceServerRelativePath.substring(sourceServerRelativePath.lastIndexOf('.'));
}
- const absoluteSourcePath = this.getAbsoluteUrl(args.options.webUrl, sourcePath);
- let absoluteTargetPath = this.getAbsoluteUrl(args.options.webUrl, args.options.targetUrl) + '/';
+ const copyJobResponse = await spo.createCopyJob(
+ args.options.webUrl,
+ sourcePath,
+ destinationPath,
+ {
+ nameConflictBehavior: this.getNameConflictBehaviorValue(args.options.nameConflictBehavior),
+ bypassSharedLock: !!args.options.bypassSharedLock,
+ includeItemPermissions: !!args.options.includeItemPermissions,
+ newName: newName,
+ operation: 'move'
+ }
+ );
+
+ if (args.options.skipWait) {
+ return;
+ }
- if (args.options.newName) {
- absoluteTargetPath += args.options.newName;
+ if (this.verbose) {
+ await logger.logToStderr('Waiting for the move job to complete...');
}
- else {
- // Keep the original file name
- absoluteTargetPath += sourcePath.substring(sourcePath.lastIndexOf('/') + 1);
+
+ const copyJobResult = await spo.getCopyJobResult(args.options.webUrl, copyJobResponse);
+
+ if (this.verbose) {
+ await logger.logToStderr('Getting information about the destination file...');
}
+ // Get destination file data
+ const siteRelativeDestinationFolder = '/' + copyJobResult.TargetObjectSiteRelativeUrl.substring(0, copyJobResult.TargetObjectSiteRelativeUrl.lastIndexOf('/'));
+ const absoluteWebUrl = destinationPath.substring(0, destinationPath.toLowerCase().lastIndexOf(siteRelativeDestinationFolder.toLowerCase()));
+
const requestOptions: CliRequestOptions = {
- url: `${args.options.webUrl}/_api/SP.MoveCopyUtil.MoveFileByPath`,
+ url: `${absoluteWebUrl}/_api/Web/GetFileById('${copyJobResult.TargetObjectUniqueId}')`,
headers: {
accept: 'application/json;odata=nometadata'
},
- responseType: 'json',
- data: {
- srcPath: {
- DecodedUrl: absoluteSourcePath
- },
- destPath: {
- DecodedUrl: absoluteTargetPath
- },
- overwrite: args.options.nameConflictBehavior === 'replace',
- options: {
- KeepBoth: args.options.nameConflictBehavior === 'rename',
- ShouldBypassSharedLocks: !!args.options.bypassSharedLock,
- RetainEditorAndModifiedOnMove: !!args.options.retainEditorAndModified
- }
- }
+ responseType: 'json'
};
- await request.post(requestOptions);
+ const destinationFile = await request.get(requestOptions);
+ await logger.log(destinationFile);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
@@ -177,19 +198,33 @@ class SpoFileMoveCommand extends SpoCommand {
}
const requestOptions: CliRequestOptions = {
- url: `${options.webUrl}/_api/Web/GetFileById('${options.sourceId}')?$select=ServerRelativePath`,
+ url: `${options.webUrl}/_api/Web/GetFileById('${options.sourceId}')/ServerRelativePath`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
- const file = await request.get<{ ServerRelativePath: { DecodedUrl: string } }>(requestOptions);
- return file.ServerRelativePath.DecodedUrl;
+ const file = await request.get<{ DecodedUrl: string }>(requestOptions);
+ return file.DecodedUrl;
+ }
+
+ private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateCopyJobsNameConflictBehavior {
+ switch (nameConflictBehavior?.toLowerCase()) {
+ case 'fail':
+ return CreateCopyJobsNameConflictBehavior.Fail;
+ case 'replace':
+ return CreateCopyJobsNameConflictBehavior.Replace;
+ case 'rename':
+ return CreateCopyJobsNameConflictBehavior.Rename;
+ default:
+ return CreateCopyJobsNameConflictBehavior.Fail;
+ }
}
private getAbsoluteUrl(webUrl: string, url: string): string {
- return url.startsWith('https://') ? url : urlUtil.getAbsoluteUrl(webUrl, url);
+ const result = url.startsWith('https://') ? url : urlUtil.getAbsoluteUrl(webUrl, url);
+ return urlUtil.removeTrailingSlashes(result);
}
}
diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts
index 871ee1a4d8d..76aea4c0ead 100644
--- a/src/utils/spo.spec.ts
+++ b/src/utils/spo.spec.ts
@@ -2720,6 +2720,8 @@ describe('utils/spo', () => {
BypassSharedLock: false,
IgnoreVersionHistory: false,
CustomizedItemName: undefined,
+ IsMoveMode: false,
+ IncludeItemPermissions: false,
SameWebCopyMoveOptimization: true
}
});
@@ -2746,7 +2748,8 @@ describe('utils/spo', () => {
nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename,
bypassSharedLock: true,
ignoreVersionHistory: true,
- newName: 'CompanyV2.png'
+ newName: 'CompanyV2.png',
+ operation: 'copy'
}
);
assert.deepStrictEqual(postStub.firstCall.args[0].data, {
@@ -2757,6 +2760,49 @@ describe('utils/spo', () => {
AllowSchemaMismatch: true,
BypassSharedLock: true,
IgnoreVersionHistory: true,
+ IsMoveMode: false,
+ IncludeItemPermissions: false,
+ CustomizedItemName: ['CompanyV2.png'],
+ SameWebCopyMoveOptimization: true
+ }
+ });
+ });
+
+ it('correctly creates a copy job with custom move options when using createCopyJob', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') {
+ return {
+ value: [
+ copyJobInfo
+ ]
+ };
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await spo.createCopyJob(
+ 'https://contoso.sharepoint.com/sites/sales',
+ 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png',
+ 'https://contoso.sharepoint.com/sites/marketing/Shared Documents',
+ {
+ nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename,
+ bypassSharedLock: true,
+ includeItemPermissions: true,
+ newName: 'CompanyV2.png',
+ operation: 'move'
+ }
+ );
+ assert.deepStrictEqual(postStub.firstCall.args[0].data, {
+ destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents',
+ exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons/Company.png'],
+ options: {
+ NameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename,
+ AllowSchemaMismatch: true,
+ BypassSharedLock: true,
+ IgnoreVersionHistory: false,
+ IsMoveMode: true,
+ IncludeItemPermissions: true,
CustomizedItemName: ['CompanyV2.png'],
SameWebCopyMoveOptimization: true
}
diff --git a/src/utils/spo.ts b/src/utils/spo.ts
index 2fc43766fc7..fd9ff135094 100644
--- a/src/utils/spo.ts
+++ b/src/utils/spo.ts
@@ -91,9 +91,13 @@ export interface User {
interface CreateCopyJobsOptions {
nameConflictBehavior?: CreateCopyJobsNameConflictBehavior;
+ newName?: string;
bypassSharedLock?: boolean;
+ /** @remarks Use only when using operation copy. */
ignoreVersionHistory?: boolean;
- newName?: string;
+ /** @remarks Use only when using operation move. */
+ includeItemPermissions?: boolean;
+ operation: 'copy' | 'move';
}
export enum CreateCopyJobsNameConflictBehavior {
@@ -1953,8 +1957,10 @@ export const spo = {
AllowSchemaMismatch: true,
BypassSharedLock: !!options?.bypassSharedLock,
IgnoreVersionHistory: !!options?.ignoreVersionHistory,
+ IncludeItemPermissions: !!options?.includeItemPermissions,
CustomizedItemName: options?.newName ? [options.newName] : undefined,
- SameWebCopyMoveOptimization: true
+ SameWebCopyMoveOptimization: true,
+ IsMoveMode: options?.operation === 'move'
}
}
};