-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds command
m365 spo site unarchive
- Loading branch information
1 parent
5ca5055
commit c2b2a8e
Showing
5 changed files
with
320 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import Global from '/docs/cmd/_global.mdx'; | ||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
# spo site unarchive | ||
|
||
Unarchives a site collection | ||
|
||
## Usage | ||
|
||
```sh | ||
m365 spo site unarchive [options] | ||
``` | ||
|
||
## Options | ||
|
||
```md definition-list | ||
`-u, --url <url>` | ||
: URL of the site collection. | ||
|
||
`-f, --force` | ||
: Don't prompt for confirmation. | ||
``` | ||
|
||
<Global /> | ||
|
||
## Remarks | ||
|
||
:::info | ||
|
||
To use this command you must be a Global or SharePoint administrator. | ||
|
||
::: | ||
|
||
:::warning | ||
|
||
If a site remains archived for more than seven days, the reactivation fee will be calculated based on the entire storage capacity of the site. | ||
|
||
::: | ||
|
||
## Examples | ||
|
||
Unarchive a specific site collection. | ||
|
||
```sh | ||
m365 spo site unarchive --url "https://contoso.sharepoint.com/sites/Marketing" | ||
``` | ||
|
||
Unarchive a specific site collection without confirmation prompt. | ||
|
||
```sh | ||
m365 spo site unarchive --url "https://contoso.sharepoint.com/sites/Marketing" --force | ||
``` | ||
|
||
## Response | ||
|
||
The command won't return a response on success. | ||
|
||
## More information | ||
|
||
Pricing model for Microsoft 365 Archive: [https://learn.microsoft.com/en-us/microsoft-365/archive/archive-pricing?view=o365-worldwide](https://learn.microsoft.com/en-us/microsoft-365/archive/archive-pricing?view=o365-worldwide) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import assert from 'assert'; | ||
import sinon from 'sinon'; | ||
import auth from '../../../../Auth.js'; | ||
import { cli } from '../../../../cli/cli.js'; | ||
import { CommandInfo } from '../../../../cli/CommandInfo.js'; | ||
import { Logger } from '../../../../cli/Logger.js'; | ||
import { telemetry } from '../../../../telemetry.js'; | ||
import { pid } from '../../../../utils/pid.js'; | ||
import { session } from '../../../../utils/session.js'; | ||
import { sinonUtil } from '../../../../utils/sinonUtil.js'; | ||
import commands from '../../commands.js'; | ||
import command from './site-unarchive.js'; | ||
import request from '../../../../request.js'; | ||
import { spo } from '../../../../utils/spo.js'; | ||
import { CommandError } from '../../../../Command.js'; | ||
|
||
describe(commands.SITE_UNARCHIVE, () => { | ||
const url = 'https://contoso.sharepoint.com/sites/project-x'; | ||
const adminUrl = 'https://contoso-admin.sharepoint.com'; | ||
const response = '[{"SchemaVersion":"15.0.0.0","LibraryVersion":"16.0.24817.12008","ErrorInfo":null,"TraceCorrelationId":"ab1127a1-5044-8000-b17c-4aafdd265386"},2,{"IsNull":false},4,{"IsNull":false}]'; | ||
|
||
let log: any[]; | ||
let logger: Logger; | ||
let commandInfo: CommandInfo; | ||
let promptIssued: boolean = false; | ||
|
||
before(() => { | ||
sinon.stub(auth, 'restoreAuth').resolves(); | ||
sinon.stub(telemetry, 'trackEvent').returns(); | ||
sinon.stub(pid, 'getProcessName').returns(''); | ||
sinon.stub(session, 'getId').returns(''); | ||
sinon.stub(spo, 'getSpoAdminUrl').resolves(adminUrl); | ||
sinon.stub(spo, 'getRequestDigest').resolves({ | ||
FormDigestValue: 'abc', | ||
FormDigestTimeoutSeconds: 1800, | ||
FormDigestExpiresAt: new Date(), | ||
WebFullUrl: 'https://contoso.sharepoint.com' | ||
}); | ||
auth.connection.active = 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); | ||
} | ||
}; | ||
|
||
sinon.stub(cli, 'promptForConfirmation').callsFake(async () => { | ||
promptIssued = true; | ||
return false; | ||
}); | ||
promptIssued = false; | ||
}); | ||
|
||
afterEach(() => { | ||
sinonUtil.restore([ | ||
cli.promptForConfirmation, | ||
request.post | ||
]); | ||
}); | ||
|
||
after(() => { | ||
sinon.restore(); | ||
auth.connection.active = false; | ||
}); | ||
|
||
it('has correct name', () => { | ||
assert.strictEqual(command.name, commands.SITE_UNARCHIVE); | ||
}); | ||
|
||
it('has a description', () => { | ||
assert.notStrictEqual(command.description, null); | ||
}); | ||
|
||
it('fails validation if url is not a valid SharePoint URL', async () => { | ||
const actual = await command.validate({ options: { url: 'invalid' } }, commandInfo); | ||
assert.notStrictEqual(actual, true); | ||
}); | ||
|
||
it('passes validation when a valid url is specified', async () => { | ||
const actual = await command.validate({ options: { url: url } }, commandInfo); | ||
assert.strictEqual(actual, true); | ||
}); | ||
|
||
it('aborts unarchiving site when prompt not confirmed', async () => { | ||
const postStub = sinon.stub(request, 'post'); | ||
|
||
await command.action(logger, { options: { url: url } }); | ||
assert(postStub.notCalled); | ||
}); | ||
|
||
it('prompts before unarchiving the site when --force option is not passed', async () => { | ||
await command.action(logger, { options: { url: url } }); | ||
assert(promptIssued); | ||
}); | ||
|
||
it('unarchives the site when prompt confirmed', async () => { | ||
sinonUtil.restore(cli.promptForConfirmation); | ||
sinon.stub(cli, 'promptForConfirmation').resolves(true); | ||
|
||
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { | ||
if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) { | ||
return response; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { url: url, verbose: true } }); | ||
assert(postStub.calledOnce); | ||
}); | ||
|
||
it('unarchives the site without prompting for confirmation when --force option specified', async () => { | ||
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { | ||
if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) { | ||
return response; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await command.action(logger, { options: { url: url, force: true, verbose: true } }); | ||
assert(postStub.called); | ||
}); | ||
|
||
it('correctly handles error when unarchiving site that does not exist', async () => { | ||
const errorMessage = 'File Not Found.'; | ||
|
||
sinon.stub(request, 'post').callsFake(async (opts) => { | ||
if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) { | ||
return `[{"SchemaVersion":"15.0.0.0","LibraryVersion":"16.0.24817.12008","ErrorInfo":{"ErrorMessage":"${errorMessage}","ErrorValue":null,"TraceCorrelationId":"731127a1-9041-8000-99fd-2865e0d78b49","ErrorCode":-2147024894,"ErrorTypeName":"System.IO.FileNotFoundException"},"TraceCorrelationId":"731127a1-9041-8000-99fd-2865e0d78b49"}]`; | ||
} | ||
|
||
throw 'Invalid request'; | ||
}); | ||
|
||
await assert.rejects(command.action(logger, { options: { url: url, force: true, verbose: true } }), | ||
new CommandError(errorMessage)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { cli } from '../../../../cli/cli.js'; | ||
import { Logger } from '../../../../cli/Logger.js'; | ||
import config from '../../../../config.js'; | ||
import GlobalOptions from '../../../../GlobalOptions.js'; | ||
import request, { CliRequestOptions } from '../../../../request.js'; | ||
import { ClientSvcResponse, ClientSvcResponseContents, spo } from '../../../../utils/spo.js'; | ||
import { validation } from '../../../../utils/validation.js'; | ||
import SpoCommand from '../../../base/SpoCommand.js'; | ||
import commands from '../../commands.js'; | ||
|
||
interface CommandArgs { | ||
options: Options; | ||
} | ||
|
||
interface Options extends GlobalOptions { | ||
url: string; | ||
force?: boolean; | ||
} | ||
|
||
class SpoSiteUnarchiveCommand extends SpoCommand { | ||
public get name(): string { | ||
return commands.SITE_UNARCHIVE; | ||
} | ||
|
||
public get description(): string { | ||
return 'Unarchives a site collection'; | ||
} | ||
|
||
constructor() { | ||
super(); | ||
|
||
this.#initOptions(); | ||
this.#initValidators(); | ||
this.#initTypes(); | ||
} | ||
|
||
#initOptions(): void { | ||
this.options.unshift( | ||
{ | ||
option: '-u, --url <url>' | ||
}, | ||
{ | ||
option: '-f, --force' | ||
} | ||
); | ||
} | ||
|
||
#initValidators(): void { | ||
this.validators.push( | ||
async (args) => validation.isValidSharePointUrl(args.options.url) | ||
); | ||
} | ||
|
||
#initTypes(): void { | ||
this.types.string.push('url'); | ||
} | ||
|
||
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { | ||
|
||
if (args.options.force) { | ||
await this.unarchiveSite(logger, args.options.url); | ||
} | ||
else { | ||
const result = await cli.promptForConfirmation({ message: `Are you sure you want to unarchive the site ${args.options.url}?` }); | ||
|
||
if (result) { | ||
await this.unarchiveSite(logger, args.options.url); | ||
} | ||
} | ||
} | ||
|
||
private async unarchiveSite(logger: Logger, url: string): Promise<void> { | ||
if (this.verbose) { | ||
await logger.logToStderr(`Unarchiving site ${url}...`); | ||
} | ||
|
||
try { | ||
const adminCenterUrl = await spo.getSpoAdminUrl(logger, this.debug); | ||
const requestDigest = await spo.getRequestDigest(adminCenterUrl); | ||
const requestData = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectPath Id="4" ObjectPathId="3" /></Actions><ObjectPaths><Constructor Id="1" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="3" ParentId="1" Name="UnarchiveSiteByUrl"><Parameters><Parameter Type="String">${url}</Parameter></Parameters></Method></ObjectPaths></Request>`; | ||
|
||
const requestOptions: CliRequestOptions = { | ||
url: `${adminCenterUrl}/_vti_bin/client.svc/ProcessQuery`, | ||
headers: { | ||
'X-RequestDigest': requestDigest.FormDigestValue | ||
}, | ||
data: requestData | ||
}; | ||
|
||
const response: string = await request.post(requestOptions); | ||
const json: ClientSvcResponse = JSON.parse(response); | ||
const responseContent: ClientSvcResponseContents = json[0]; | ||
|
||
if (responseContent.ErrorInfo) { | ||
throw responseContent.ErrorInfo.ErrorMessage; | ||
} | ||
} | ||
catch (err: any) { | ||
this.handleRejectedPromise(err); | ||
} | ||
} | ||
} | ||
|
||
export default new SpoSiteUnarchiveCommand(); |