@fluid-tools/fetch-tool 0.53.0-46105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.eslintrc.js +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +114 -0
  4. package/bin/fluid-fetch +2 -0
  5. package/dist/fluidAnalyzeMessages.d.ts +8 -0
  6. package/dist/fluidAnalyzeMessages.d.ts.map +1 -0
  7. package/dist/fluidAnalyzeMessages.js +598 -0
  8. package/dist/fluidAnalyzeMessages.js.map +1 -0
  9. package/dist/fluidFetch.d.ts +6 -0
  10. package/dist/fluidFetch.d.ts.map +1 -0
  11. package/dist/fluidFetch.js +119 -0
  12. package/dist/fluidFetch.js.map +1 -0
  13. package/dist/fluidFetchArgs.d.ts +35 -0
  14. package/dist/fluidFetchArgs.d.ts.map +1 -0
  15. package/dist/fluidFetchArgs.js +206 -0
  16. package/dist/fluidFetchArgs.js.map +1 -0
  17. package/dist/fluidFetchInit.d.ts +9 -0
  18. package/dist/fluidFetchInit.d.ts.map +1 -0
  19. package/dist/fluidFetchInit.js +161 -0
  20. package/dist/fluidFetchInit.js.map +1 -0
  21. package/dist/fluidFetchMessages.d.ts +7 -0
  22. package/dist/fluidFetchMessages.d.ts.map +1 -0
  23. package/dist/fluidFetchMessages.js +264 -0
  24. package/dist/fluidFetchMessages.js.map +1 -0
  25. package/dist/fluidFetchSharePoint.d.ts +10 -0
  26. package/dist/fluidFetchSharePoint.d.ts.map +1 -0
  27. package/dist/fluidFetchSharePoint.js +95 -0
  28. package/dist/fluidFetchSharePoint.js.map +1 -0
  29. package/dist/fluidFetchSnapshot.d.ts +7 -0
  30. package/dist/fluidFetchSnapshot.d.ts.map +1 -0
  31. package/dist/fluidFetchSnapshot.js +289 -0
  32. package/dist/fluidFetchSnapshot.js.map +1 -0
  33. package/package.json +65 -0
  34. package/src/fluidAnalyzeMessages.ts +687 -0
  35. package/src/fluidFetch.ts +123 -0
  36. package/src/fluidFetchArgs.ts +224 -0
  37. package/src/fluidFetchInit.ts +168 -0
  38. package/src/fluidFetchMessages.ts +280 -0
  39. package/src/fluidFetchSharePoint.ts +141 -0
  40. package/src/fluidFetchSnapshot.ts +383 -0
  41. package/tsconfig.json +17 -0
@@ -0,0 +1,280 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import fs from "fs";
7
+ import { assert } from "@fluidframework/common-utils";
8
+ import {
9
+ IDocumentService,
10
+ } from "@fluidframework/driver-definitions";
11
+ import {
12
+ IClient,
13
+ ISequencedDocumentMessage,
14
+ MessageType,
15
+ ScopeType,
16
+ } from "@fluidframework/protocol-definitions";
17
+ import { printMessageStats } from "./fluidAnalyzeMessages";
18
+ import {
19
+ connectToWebSocket,
20
+ dumpMessages,
21
+ dumpMessageStats,
22
+ overWrite,
23
+ paramActualFormatting,
24
+ messageTypeFilter,
25
+ } from "./fluidFetchArgs";
26
+
27
+ function filenameFromIndex(index: number): string {
28
+ return index === 0 ? "" : index.toString(); // support old tools...
29
+ }
30
+
31
+ let firstAvailableDelta = 1;
32
+ async function* loadAllSequencedMessages(
33
+ documentService?: IDocumentService,
34
+ dir?: string,
35
+ files?: string[]) {
36
+ let lastSeq = 0;
37
+ // flag for mismatch between last sequence number read and new one to be read
38
+ let seqNumMismatch = false;
39
+
40
+ // If we have local save, read ops from there first
41
+ if (files !== undefined) {
42
+ for (let i = 0; i < files.length; i++) {
43
+ const file = filenameFromIndex(i);
44
+ try {
45
+ console.log(`reading messages${file}.json`);
46
+ const fileContent = fs.readFileSync(`${dir}/messages${file}.json`, { encoding: "utf-8" });
47
+ const messages: ISequencedDocumentMessage[] = JSON.parse(fileContent);
48
+ // check if there is mismatch
49
+ seqNumMismatch = messages[0].sequenceNumber !== lastSeq + 1;
50
+ assert(!seqNumMismatch, 0x1b9 /* "Unexpected value for sequence number of first message in file" */);
51
+ lastSeq = messages[messages.length - 1].sequenceNumber;
52
+ yield messages;
53
+ } catch (e) {
54
+ if (seqNumMismatch) {
55
+ if (overWrite) {
56
+ // with overWrite option on, we will delete all exisintg message.json files
57
+ for (let index = 0; index < files.length; index++) {
58
+ const name = filenameFromIndex(index);
59
+ fs.unlinkSync(`${dir}/messages${name}.json`);
60
+ }
61
+ break;
62
+ }
63
+ // prompt user to back up and delete existing files
64
+ console.error("There are deleted ops in the document being requested," +
65
+ " please back up the existing messages.json file and delete it from its directory." +
66
+ " Then try fetch tool again.");
67
+ console.error(e);
68
+ return;
69
+ } else {
70
+ console.error(`Error reading / parsing messages from ${files}`);
71
+ console.error(e);
72
+ return;
73
+ }
74
+ }
75
+ }
76
+ if (lastSeq !== 0) {
77
+ console.log(`Read ${lastSeq} ops from local cache`);
78
+ }
79
+ }
80
+
81
+ if (!documentService) {
82
+ return;
83
+ }
84
+
85
+ const deltaStorage = await documentService.connectToDeltaStorage();
86
+
87
+ let timeStart = Date.now();
88
+ let requests = 0;
89
+ let opsStorage = 0;
90
+
91
+ // reading only 1 op to test if there is mismatch
92
+ const teststream = deltaStorage.fetchMessages(
93
+ lastSeq + 1,
94
+ lastSeq + 2);
95
+
96
+ let statusCode;
97
+ let innerMostErrorCode;
98
+ let response;
99
+
100
+ try {
101
+ await teststream.read();
102
+ } catch (error) {
103
+ statusCode = error.getTelemetryProperties().statusCode;
104
+ innerMostErrorCode = error.getTelemetryProperties().innerMostErrorCode;
105
+ // if there is gap between ops, catch the error and check it is the error we need
106
+ if (statusCode !== 410 || innerMostErrorCode !== "fluidDeltaDataNotAvailable") {
107
+ throw error;
108
+ }
109
+ // get firstAvailableDelta from the error response, and set current sequence number to that
110
+ response = JSON.parse(error.getTelemetryProperties().response);
111
+ firstAvailableDelta = response.error.firstAvailableDelta;
112
+ lastSeq = firstAvailableDelta - 1;
113
+ }
114
+
115
+ // continue reading rest of the ops
116
+ const stream = deltaStorage.fetchMessages(
117
+ lastSeq + 1, // inclusive left
118
+ undefined, // to
119
+ );
120
+
121
+ while (true) {
122
+ const result = await stream.read();
123
+ if (result.done) {
124
+ break;
125
+ }
126
+ requests++;
127
+ const messages = result.value;
128
+
129
+ // Empty buckets should never be returned
130
+ assert(messages.length !== 0, 0x1ba /* "should not return empty buckets" */);
131
+ // console.log(`Loaded ops at ${messages[0].sequenceNumber}`);
132
+
133
+ // This parsing of message contents happens in delta manager. But when we analyze messages
134
+ // for message stats, we skip that path. So parsing of json contents needs to happen here.
135
+ for (const message of messages) {
136
+ if (typeof message.contents === "string"
137
+ && message.contents !== ""
138
+ && message.type !== MessageType.ClientLeave
139
+ ) {
140
+ message.contents = JSON.parse(message.contents);
141
+ }
142
+ }
143
+
144
+ opsStorage += messages.length;
145
+ lastSeq = messages[messages.length - 1].sequenceNumber;
146
+ yield messages;
147
+ }
148
+
149
+ // eslint-disable-next-line max-len
150
+ console.log(`\n${Math.floor((Date.now() - timeStart) / 1000)} seconds to retrieve ${opsStorage} ops in ${requests} requests`);
151
+
152
+ if (connectToWebSocket) {
153
+ let logMsg = "";
154
+ const client: IClient = {
155
+ mode: "write",
156
+ permission: [],
157
+ scopes: [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
158
+ details: {
159
+ capabilities: { interactive: true },
160
+ },
161
+ user: { id: "blah" },
162
+ };
163
+ console.log("Retrieving messages from web socket");
164
+ timeStart = Date.now();
165
+ const deltaStream = await documentService.connectToDeltaStream(client);
166
+ const initialMessages = deltaStream.initialMessages;
167
+ deltaStream.dispose();
168
+ console.log(`${Math.floor((Date.now() - timeStart) / 1000)} seconds to connect to web socket`);
169
+
170
+ if (initialMessages) {
171
+ const lastSequenceNumber = lastSeq;
172
+ const filtered = initialMessages.filter((a) => a.sequenceNumber > lastSequenceNumber);
173
+ const sorted = filtered.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
174
+ lastSeq = sorted[sorted.length - 1].sequenceNumber;
175
+ // eslint-disable-next-line max-len
176
+ logMsg = ` (${opsStorage} delta storage, ${initialMessages.length} initial ws messages, ${initialMessages.length - sorted.length} dup)`;
177
+ yield sorted;
178
+ }
179
+ console.log(`${lastSeq} total messages${logMsg}`);
180
+ }
181
+ }
182
+
183
+ async function* saveOps(
184
+ gen, // AsyncGenerator<ISequencedDocumentMessage[]>,
185
+ dir: string,
186
+ files: string[]) {
187
+ // Split into 100K ops
188
+ const chunk = 100 * 1000;
189
+
190
+ let sequencedMessages: ISequencedDocumentMessage[] = [];
191
+
192
+ // Figure out first file we want to write to
193
+ let index = 0;
194
+ let curr: number = 1;
195
+ if (files.length !== 0) {
196
+ index = files.length - 1;
197
+ const name = filenameFromIndex(index);
198
+ const fileContent = fs.readFileSync(`${dir}/messages${name}.json`, { encoding: "utf-8" });
199
+ const messages: ISequencedDocumentMessage[] = JSON.parse(fileContent);
200
+ curr = messages[0].sequenceNumber;
201
+ }
202
+
203
+ while (true) {
204
+ const result: IteratorResult<ISequencedDocumentMessage[]> = await gen.next();
205
+ if (files.length === 0) {
206
+ curr = firstAvailableDelta;
207
+ }
208
+ if (!result.done) {
209
+ let messages = result.value;
210
+ yield messages;
211
+ if (messages[messages.length - 1].sequenceNumber < curr) {
212
+ // Nothing interesting.
213
+ continue;
214
+ }
215
+ if (messages[0].sequenceNumber < curr) {
216
+ messages = messages.filter((msg) => msg.sequenceNumber >= curr);
217
+ }
218
+ sequencedMessages = sequencedMessages.concat(messages);
219
+ assert(sequencedMessages[0].sequenceNumber === curr,
220
+ 0x1bb /* "Unexpected sequence number on first of messages to save" */);
221
+ assert(sequencedMessages[sequencedMessages.length - 1].sequenceNumber
222
+ === curr + sequencedMessages.length - 1,
223
+ 0x1bc /* "Unexpected sequence number on last of messages to save" */);
224
+ }
225
+
226
+ // Time to write it out?
227
+ while (sequencedMessages.length >= chunk || (result.done && sequencedMessages.length !== 0)) {
228
+ const name = filenameFromIndex(index);
229
+ const write = sequencedMessages.splice(0, chunk);
230
+ console.log(`writing messages${name}.json`);
231
+ fs.writeFileSync(
232
+ `${dir}/messages${name}.json`,
233
+ JSON.stringify(write, undefined, paramActualFormatting ? 0 : 2));
234
+ // increment curr by chunk
235
+ curr += chunk;
236
+ assert(sequencedMessages.length === 0 || sequencedMessages[0].sequenceNumber === curr,
237
+ 0x1bd /* "Stopped writing at unexpected sequence number" */);
238
+ index++;
239
+ }
240
+
241
+ if (result.done) {
242
+ break;
243
+ }
244
+ }
245
+ }
246
+
247
+ export async function fluidFetchMessages(documentService?: IDocumentService, saveDir?: string) {
248
+ const messageStats = dumpMessageStats || dumpMessages;
249
+ if (!messageStats && (saveDir === undefined || documentService === undefined)) {
250
+ return;
251
+ }
252
+
253
+ const files = !saveDir ?
254
+ undefined :
255
+ fs.readdirSync(saveDir)
256
+ .filter((file) => {
257
+ if (!file.startsWith("messages")) {
258
+ return false;
259
+ }
260
+ return true;
261
+ })
262
+ .sort((a, b) => a.localeCompare(b));
263
+
264
+ let generator = loadAllSequencedMessages(documentService, saveDir, files);
265
+
266
+ if (saveDir && files !== undefined && documentService) {
267
+ generator = saveOps(generator, saveDir, files);
268
+ }
269
+
270
+ if (messageStats) {
271
+ return printMessageStats(
272
+ generator,
273
+ dumpMessageStats,
274
+ dumpMessages,
275
+ messageTypeFilter);
276
+ } else {
277
+ let item;
278
+ for await (item of generator) { }
279
+ }
280
+ }
@@ -0,0 +1,141 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { DriverErrorType } from "@fluidframework/driver-definitions";
7
+ import {
8
+ getChildrenByDriveItem,
9
+ getDriveItemByServerRelativePath,
10
+ getDriveItemFromDriveAndItem,
11
+ IClientConfig,
12
+ IOdspDriveItem,
13
+ getOdspRefreshTokenFn,
14
+ IOdspAuthRequestInfo,
15
+ } from "@fluidframework/odsp-doclib-utils";
16
+ import {
17
+ getMicrosoftConfiguration,
18
+ OdspTokenManager,
19
+ odspTokensCache,
20
+ OdspTokenConfig,
21
+ IOdspTokenManagerCacheKey,
22
+ } from "@fluidframework/tool-utils";
23
+ import { fluidFetchWebNavigator } from "./fluidFetchInit";
24
+ import { getForceTokenReauth } from "./fluidFetchArgs";
25
+
26
+ export async function resolveWrapper<T>(
27
+ callback: (authRequestInfo: IOdspAuthRequestInfo) => Promise<T>,
28
+ server: string,
29
+ clientConfig: IClientConfig,
30
+ forceTokenReauth = false,
31
+ forToken = false,
32
+ ): Promise<T> {
33
+ try {
34
+ const odspTokenManager = new OdspTokenManager(odspTokensCache);
35
+ const tokenConfig: OdspTokenConfig = {
36
+ type: "browserLogin",
37
+ navigator: fluidFetchWebNavigator,
38
+ };
39
+ const tokens = await odspTokenManager.getOdspTokens(
40
+ server,
41
+ clientConfig,
42
+ tokenConfig,
43
+ undefined /* forceRefresh */,
44
+ forceTokenReauth || getForceTokenReauth(),
45
+ );
46
+
47
+ const result = await callback({
48
+ accessToken: tokens.accessToken,
49
+ refreshTokenFn: getOdspRefreshTokenFn(server, clientConfig, tokens),
50
+ });
51
+ // If this is used for getting a token, then refresh the cache with new token.
52
+ if (forToken) {
53
+ const key: IOdspTokenManagerCacheKey = { isPush: false, userOrServer: server };
54
+ await odspTokenManager.updateTokensCache(
55
+ key, { accessToken: result as any as string, refreshToken: tokens.refreshToken });
56
+ return result;
57
+ }
58
+ return result;
59
+ } catch (e) {
60
+ if (e.errorType === DriverErrorType.authorizationError && !forceTokenReauth) {
61
+ // Re-auth
62
+ return resolveWrapper<T>(callback, server, clientConfig, true, forToken);
63
+ }
64
+ throw e;
65
+ }
66
+ }
67
+
68
+ export async function resolveDriveItemByServerRelativePath(
69
+ server: string,
70
+ serverRelativePath: string,
71
+ clientConfig: IClientConfig,
72
+ ) {
73
+ return resolveWrapper<IOdspDriveItem>(
74
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
75
+ (authRequestInfo) => getDriveItemByServerRelativePath(
76
+ server,
77
+ serverRelativePath,
78
+ authRequestInfo,
79
+ false,
80
+ ),
81
+ server, clientConfig);
82
+ }
83
+
84
+ async function resolveChildrenByDriveItem(
85
+ server: string,
86
+ folderDriveItem: IOdspDriveItem,
87
+ clientConfig: IClientConfig,
88
+ ) {
89
+ return resolveWrapper<IOdspDriveItem[]>(
90
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
91
+ (authRequestInfo) => getChildrenByDriveItem(folderDriveItem, server, authRequestInfo),
92
+ server, clientConfig);
93
+ }
94
+
95
+ export async function getSharepointFiles(server: string, serverRelativePath: string, recurse: boolean) {
96
+ const clientConfig = getMicrosoftConfiguration();
97
+
98
+ const fileInfo = await resolveDriveItemByServerRelativePath(server, serverRelativePath, clientConfig);
99
+ console.log(fileInfo);
100
+ const pendingFolder: { path: string, folder: IOdspDriveItem }[] = [];
101
+ const files: IOdspDriveItem[] = [];
102
+ if (fileInfo.isFolder) {
103
+ pendingFolder.push({ path: serverRelativePath, folder: fileInfo });
104
+ } else {
105
+ files.push(fileInfo);
106
+ }
107
+
108
+ // eslint-disable-next-line no-constant-condition
109
+ while (true) {
110
+ const folderInfo = pendingFolder.shift();
111
+ if (!folderInfo) { break; }
112
+ const { path, folder } = folderInfo;
113
+ const children = await resolveChildrenByDriveItem(server, folder, clientConfig);
114
+ for (const child of children) {
115
+ const childPath = `${path}/${child.name}`;
116
+ if (child.isFolder) {
117
+ if (recurse) {
118
+ pendingFolder.push({ path: childPath, folder: child });
119
+ }
120
+ } else {
121
+ files.push(child);
122
+ }
123
+ }
124
+ }
125
+ return files;
126
+ }
127
+
128
+ export async function getSingleSharePointFile(
129
+ server: string,
130
+ drive: string,
131
+ item: string,
132
+ ) {
133
+ const clientConfig = getMicrosoftConfiguration();
134
+
135
+ return resolveWrapper<IOdspDriveItem>(
136
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
137
+ (authRequestInfo) => getDriveItemFromDriveAndItem(server, drive, item, authRequestInfo),
138
+ server,
139
+ clientConfig,
140
+ );
141
+ }