Skip to content

Commit e409917

Browse files
MathijsVerbeeckmilanholemans
authored andcommitted
Adds 'spo file sharinglink list' command. Closes pnp#4023
1 parent 8e72dc4 commit e409917

File tree

8 files changed

+472
-2
lines changed

8 files changed

+472
-2
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const dictionary = [
3333
'home',
3434
'hub',
3535
'in',
36+
'info',
3637
'inheritance',
3738
'init',
3839
'install',
@@ -41,6 +42,7 @@ const dictionary = [
4142
'issue',
4243
'label',
4344
'list',
45+
'link',
4446
'management',
4547
'member',
4648
'messaging',
@@ -63,6 +65,7 @@ const dictionary = [
6365
'service',
6466
'setting',
6567
'settings',
68+
'sharing',
6669
'side',
6770
'site',
6871
'status',
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# spo file sharinglink list
2+
3+
Lists all the sharing links of a specific file
4+
5+
## Usage
6+
7+
```sh
8+
m365 spo file sharinglink list [options]
9+
```
10+
11+
## Options
12+
13+
`-u, --webUrl <webUrl>`
14+
: The URL of the site where the file is located.
15+
16+
`-f, --fileUrl [fileUrl]`
17+
: The server-relative (decoded) URL of the file. Specify either `fileUrl` or `fileId` but not both.
18+
19+
`-i, --fileId [fileId]`
20+
: The UniqueId (GUID) of the file. Specify either `fileUrl` or `fileId` but not both.
21+
22+
--8<-- "docs/cmd/_global.md"
23+
24+
## Examples
25+
26+
List sharing links of a file by id
27+
28+
```sh
29+
m365 spo file sharinglink list --webUrl https://contoso.sharepoint.com/sites/demo --fileId daebb04b-a773-4baa-b1d1-3625418e3234
30+
```
31+
32+
List sharing links of a file by url
33+
34+
```sh
35+
m365 spo file sharinglink list --webUrl https://contoso.sharepoint.com/sites/demo --fileUrl "/sites/demo/shared documents/document.docx"
36+
```
37+
38+
## Response
39+
40+
=== "JSON"
41+
42+
```json
43+
[
44+
{
45+
"id": "2a021f54-90a2-4016-b3b3-5f34d2e7d932",
46+
"roles": [
47+
"read"
48+
],
49+
"hasPassword": false,
50+
"grantedToIdentitiesV2": [
51+
{
52+
"user": {
53+
"displayName": "John Doe",
54+
"email": "[email protected]",
55+
"id": "fe36f75e-c103-410b-a18a-2bf6df06ac3a"
56+
},
57+
"siteUser": {
58+
"displayName": "John Doe",
59+
"email": "[email protected]",
60+
"id": "9",
61+
"loginName": "i:0#.f|membership|[email protected]"
62+
}
63+
}
64+
],
65+
"grantedToIdentities": [
66+
{
67+
"user": {
68+
"displayName": "John Doe",
69+
"email": "[email protected]",
70+
"id": "fe36f75e-c103-410b-a18a-2bf6df06ac3a"
71+
}
72+
}
73+
],
74+
"link": {
75+
"scope": "anonymous",
76+
"type": "view",
77+
"webUrl": "https://contoso.sharepoint.com/:b:/s/demo/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A",
78+
"preventsDownload": false
79+
}
80+
}
81+
]
82+
```
83+
84+
=== "Text"
85+
86+
```text
87+
id roles link
88+
------------------------------------ ----- -----------------------------------------------------------------------------------------------------------
89+
2a021f54-90a2-4016-b3b3-5f34d2e7d932 read https://contoso.sharepoint.com/:b:/s/demo/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A
90+
```
91+
92+
=== "CSV"
93+
94+
```csv
95+
id,roles,link
96+
2a021f54-90a2-4016-b3b3-5f34d2e7d932,read,https://contoso.sharepoint.com/:b:/s/demo/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A
97+
```

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ nav:
401401
- file roleinheritance break: cmd/spo/file/file-roleinheritance-break.md
402402
- file roleinheritance reset: cmd/spo/file/file-roleinheritance-reset.md
403403
- file sharinginfo get: cmd/spo/file/file-sharinginfo-get.md
404+
- file sharinglink list: cmd/spo/file/file-sharinglink-list.md
404405
- file version clear: cmd/spo/file/file-version-clear.md
405406
- file version get: cmd/spo/file/file-version-get.md
406407
- file version list: cmd/spo/file/file-version-list.md

src/m365/spo/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default {
6161
FILE_ROLEINHERITANCE_BREAK: `${prefix} file roleinheritance break`,
6262
FILE_ROLEINHERITANCE_RESET: `${prefix} file roleinheritance reset`,
6363
FILE_SHARINGINFO_GET: `${prefix} file sharinginfo get`,
64+
FILE_SHARINGLINK_LIST: `${prefix} file sharinglink list`,
6465
FILE_VERSION_CLEAR: `${prefix} file version clear`,
6566
FILE_VERSION_GET: `${prefix} file version get`,
6667
FILE_VERSION_LIST: `${prefix} file version list`,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface GraphFileDetails {
2+
SiteId: string;
3+
VroomDriveID: string;
4+
VroomItemID: string;
5+
}

src/m365/spo/commands/file/file-sharinginfo-get.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ interface FileSharingInformation {
4646
SharedWith: string;
4747
}
4848

49-
class SpoFileSharinginfoGetCommand extends SpoCommand {
49+
class SpoFileSharingInfoGetCommand extends SpoCommand {
5050
public get name(): string {
5151
return commands.FILE_SHARINGINFO_GET;
5252
}
@@ -195,4 +195,4 @@ class SpoFileSharinginfoGetCommand extends SpoCommand {
195195
}
196196
}
197197

198-
module.exports = new SpoFileSharinginfoGetCommand();
198+
module.exports = new SpoFileSharingInfoGetCommand();
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import { telemetry } from '../../../../telemetry';
4+
import auth from '../../../../Auth';
5+
import { Cli } from '../../../../cli/Cli';
6+
import { CommandInfo } from '../../../../cli/CommandInfo';
7+
import { Logger } from '../../../../cli/Logger';
8+
import Command, { CommandError } from '../../../../Command';
9+
import request from '../../../../request';
10+
import { pid } from '../../../../utils/pid';
11+
import { sinonUtil } from '../../../../utils/sinonUtil';
12+
import commands from '../../commands';
13+
import { formatting } from '../../../../utils/formatting';
14+
import { GraphFileDetails } from './GraphFileDetails';
15+
import { odata } from '../../../../utils/odata';
16+
const command: Command = require('./file-sharinglink-list');
17+
18+
describe(commands.FILE_SHARINGLINK_LIST, () => {
19+
let log: any[];
20+
let logger: Logger;
21+
let loggerLogSpy: sinon.SinonSpy;
22+
let commandInfo: CommandInfo;
23+
24+
const webUrl = 'https://contoso.sharepoint.com';
25+
const fileId = 'f09c4efe-b8c0-4e89-a166-03418661b89b';
26+
const fileUrl = '/sites/project-x/documents/SharedFile.docx';
27+
const fileDetailsResponse: GraphFileDetails = {
28+
SiteId: "0f9b8f4f-0e8e-4630-bb0a-501442db9b64",
29+
VroomItemID: "013TMHP6UOOSLON57HT5GLKEU7R5UGWZVK",
30+
VroomDriveID: "b!T4-bD44OMEa7ClAUQtubZID9tc40pGJKpguycvELod_Gx-lo4ZQiRJ7vylonTufG"
31+
};
32+
const graphResponse = {
33+
value: [
34+
{
35+
"id": "2a021f54-90a2-4016-b3b3-5f34d2e7d932",
36+
"roles": [
37+
"read"
38+
],
39+
"hasPassword": false,
40+
"grantedToIdentitiesV2": [],
41+
"grantedToIdentities": [],
42+
"link": {
43+
"scope": "anonymous",
44+
"type": "view",
45+
"webUrl": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A",
46+
"preventsDownload": false
47+
}
48+
},
49+
{
50+
"id": "a47e5387-8868-497c-bb00-115c66c60390",
51+
"roles": [
52+
"read"
53+
],
54+
"hasPassword": true,
55+
"grantedToIdentitiesV2": [],
56+
"grantedToIdentities": [],
57+
"link": {
58+
"scope": "anonymous",
59+
"type": "view",
60+
"webUrl": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBsS_o5pIcCyNIL3D_vEyG5Q",
61+
"preventsDownload": true
62+
}
63+
},
64+
{
65+
"id": "8bf1ca81-a63f-4796-9af5-d86ded8ce5a7",
66+
"roles": [
67+
"write"
68+
],
69+
"hasPassword": true,
70+
"grantedToIdentitiesV2": [],
71+
"grantedToIdentities": [],
72+
"link": {
73+
"scope": "anonymous",
74+
"type": "edit",
75+
"webUrl": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBDyAMq6f9C2eqWwFsbei6nA",
76+
"preventsDownload": false
77+
}
78+
}
79+
]
80+
};
81+
82+
const graphResponseText: any = [
83+
{
84+
"id": "2a021f54-90a2-4016-b3b3-5f34d2e7d932",
85+
"roles": "read",
86+
"link": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A"
87+
},
88+
{
89+
"id": "a47e5387-8868-497c-bb00-115c66c60390",
90+
"roles": "read",
91+
"link": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBsS_o5pIcCyNIL3D_vEyG5Q"
92+
},
93+
{
94+
"id": "8bf1ca81-a63f-4796-9af5-d86ded8ce5a7",
95+
"roles": "write",
96+
"link": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBDyAMq6f9C2eqWwFsbei6nA"
97+
}
98+
];
99+
100+
const stubOdataResponse: any = (graphResponse: any = null) => {
101+
return sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => {
102+
if (url === `https://graph.microsoft.com/v1.0/sites/${fileDetailsResponse.SiteId}/drives/${fileDetailsResponse.VroomDriveID}/items/${fileDetailsResponse.VroomItemID}/permissions?$filter=Link ne null`) {
103+
return graphResponse.value;
104+
}
105+
throw 'Invalid request';
106+
});
107+
};
108+
109+
before(() => {
110+
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
111+
sinon.stub(telemetry, 'trackEvent').callsFake(() => { });
112+
sinon.stub(pid, 'getProcessName').callsFake(() => '');
113+
auth.service.connected = true;
114+
commandInfo = Cli.getCommandInfo(command);
115+
});
116+
117+
beforeEach(() => {
118+
log = [];
119+
logger = {
120+
log: (msg: string) => {
121+
log.push(msg);
122+
},
123+
logRaw: (msg: string) => {
124+
log.push(msg);
125+
},
126+
logToStderr: (msg: string) => {
127+
log.push(msg);
128+
}
129+
};
130+
loggerLogSpy = sinon.spy(logger, 'log');
131+
});
132+
133+
afterEach(() => {
134+
sinonUtil.restore([
135+
request.get,
136+
odata.getAllItems
137+
]);
138+
});
139+
140+
after(() => {
141+
sinonUtil.restore([
142+
auth.restoreAuth,
143+
telemetry.trackEvent,
144+
pid.getProcessName
145+
]);
146+
auth.service.connected = false;
147+
});
148+
149+
it('has correct name', () => {
150+
assert.strictEqual(command.name, commands.FILE_SHARINGLINK_LIST);
151+
});
152+
153+
it('has a description', () => {
154+
assert.notStrictEqual(command.description, null);
155+
});
156+
157+
it('retrieves sharing links from file specified by id', async () => {
158+
stubOdataResponse(graphResponse);
159+
sinon.stub(request, 'get').callsFake(async (opts) => {
160+
if (opts.url === `${webUrl}/_api/web/GetFileById('${fileId}')?$select=SiteId,VroomItemId,VroomDriveId`) {
161+
return fileDetailsResponse;
162+
}
163+
164+
throw 'Invalid request';
165+
});
166+
167+
await command.action(logger, { options: { webUrl: webUrl, fileId: fileId, output: 'json', verbose: true } } as any);
168+
assert(loggerLogSpy.calledWith(graphResponse.value));
169+
});
170+
171+
it('retrieves sharing links from file specified by url', async () => {
172+
stubOdataResponse(graphResponse);
173+
sinon.stub(request, 'get').callsFake(async (opts) => {
174+
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(fileUrl)}')?$select=SiteId,VroomItemId,VroomDriveId`) {
175+
return fileDetailsResponse;
176+
}
177+
178+
throw 'Invalid request';
179+
});
180+
181+
await command.action(logger, { options: { webUrl: webUrl, fileUrl: fileUrl, output: 'json', verbose: true } } as any);
182+
assert(loggerLogSpy.calledWith(graphResponse.value));
183+
});
184+
185+
it('retrieves sharing links from file specified by url with output text', async () => {
186+
stubOdataResponse(graphResponse);
187+
sinon.stub(request, 'get').callsFake(async (opts) => {
188+
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(fileUrl)}')?$select=SiteId,VroomItemId,VroomDriveId`) {
189+
return fileDetailsResponse;
190+
}
191+
192+
throw 'Invalid request';
193+
});
194+
195+
await command.action(logger, { options: { webUrl: webUrl, fileUrl: fileUrl, output: 'text', verbose: true } } as any);
196+
assert(loggerLogSpy.calledWith(graphResponseText));
197+
});
198+
199+
it('throws error when file not found by id', async () => {
200+
sinon.stub(request, 'get').callsFake(async (opts) => {
201+
if (opts.url === `${webUrl}/_api/web/GetFileById('${fileId}')?$select=SiteId,VroomItemId,VroomDriveId`) {
202+
throw { error: { 'odata.error': { message: { value: 'File Not Found.' } } } };
203+
}
204+
205+
throw 'Invalid request';
206+
});
207+
208+
await assert.rejects(command.action(logger, { options: { webUrl: webUrl, fileId: fileId, verbose: true } } as any),
209+
new CommandError(`File Not Found.`));
210+
});
211+
212+
it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => {
213+
const actual = await command.validate({ options: { webUrl: 'foo', fileId: fileId } }, commandInfo);
214+
assert.notStrictEqual(actual, true);
215+
});
216+
217+
it('fails validation if the fileId option is not a valid GUID', async () => {
218+
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', fileId: 'invalid' } }, commandInfo);
219+
assert.notStrictEqual(actual, true);
220+
});
221+
222+
it('passes validation if options are valid', async () => {
223+
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', fileId: fileId } }, commandInfo);
224+
assert.strictEqual(actual, true);
225+
});
226+
});

0 commit comments

Comments
 (0)