Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/backup/electron-main/backupMainService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { createHash } from 'crypto';
7
import { isEqual } from '../../../base/common/extpath.js';
8
import { Schemas } from '../../../base/common/network.js';
9
import { join } from '../../../base/common/path.js';
10
import { isLinux } from '../../../base/common/platform.js';
11
import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';
12
import { Promises, RimRafMode } from '../../../base/node/pfs.js';
13
import { IBackupMainService } from './backup.js';
14
import { ISerializedBackupWorkspaces, IEmptyWindowBackupInfo, isEmptyWindowBackupInfo, deserializeWorkspaceInfos, deserializeFolderInfos, ISerializedWorkspaceBackupInfo, ISerializedFolderBackupInfo, ISerializedEmptyWindowBackupInfo } from '../node/backup.js';
15
import { IConfigurationService } from '../../configuration/common/configuration.js';
16
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
17
import { IStateService } from '../../state/node/state.js';
18
import { HotExitConfiguration, IFilesConfiguration } from '../../files/common/files.js';
19
import { ILogService } from '../../log/common/log.js';
20
import { IFolderBackupInfo, isFolderBackupInfo, IWorkspaceBackupInfo } from '../common/backup.js';
21
import { isWorkspaceIdentifier } from '../../workspace/common/workspace.js';
22
import { createEmptyWorkspaceIdentifier } from '../../workspaces/node/workspaces.js';
23
24
export class BackupMainService implements IBackupMainService {
25
26
declare readonly _serviceBrand: undefined;
27
28
private static readonly backupWorkspacesMetadataStorageKey = 'backupWorkspaces';
29
30
protected backupHome: string;
31
32
private workspaces: IWorkspaceBackupInfo[] = [];
33
private folders: IFolderBackupInfo[] = [];
34
private emptyWindows: IEmptyWindowBackupInfo[] = [];
35
36
// Comparers for paths and resources that will
37
// - ignore path casing on Windows/macOS
38
// - respect path casing on Linux
39
private readonly backupUriComparer = extUriBiasedIgnorePathCase;
40
private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) };
41
42
constructor(
43
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
44
@IConfigurationService private readonly configurationService: IConfigurationService,
45
@ILogService private readonly logService: ILogService,
46
@IStateService private readonly stateService: IStateService
47
) {
48
this.backupHome = environmentMainService.backupHome;
49
}
50
51
async initialize(): Promise<void> {
52
53
// read backup workspaces
54
const serializedBackupWorkspaces = this.stateService.getItem<ISerializedBackupWorkspaces>(BackupMainService.backupWorkspacesMetadataStorageKey) ?? { workspaces: [], folders: [], emptyWindows: [] };
55
56
// validate empty workspaces backups first
57
this.emptyWindows = await this.validateEmptyWorkspaces(serializedBackupWorkspaces.emptyWindows);
58
59
// validate workspace backups
60
this.workspaces = await this.validateWorkspaces(deserializeWorkspaceInfos(serializedBackupWorkspaces));
61
62
// validate folder backups
63
this.folders = await this.validateFolders(deserializeFolderInfos(serializedBackupWorkspaces));
64
65
// store metadata in case some workspaces or folders have been removed
66
this.storeWorkspacesMetadata();
67
}
68
69
protected getWorkspaceBackups(): IWorkspaceBackupInfo[] {
70
if (this.isHotExitOnExitAndWindowClose()) {
71
// Only non-folder windows are restored on main process launch when
72
// hot exit is configured as onExitAndWindowClose.
73
return [];
74
}
75
76
return this.workspaces.slice(0); // return a copy
77
}
78
79
protected getFolderBackups(): IFolderBackupInfo[] {
80
if (this.isHotExitOnExitAndWindowClose()) {
81
// Only non-folder windows are restored on main process launch when
82
// hot exit is configured as onExitAndWindowClose.
83
return [];
84
}
85
86
return this.folders.slice(0); // return a copy
87
}
88
89
isHotExitEnabled(): boolean {
90
return this.getHotExitConfig() !== HotExitConfiguration.OFF;
91
}
92
93
private isHotExitOnExitAndWindowClose(): boolean {
94
return this.getHotExitConfig() === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE;
95
}
96
97
private getHotExitConfig(): string {
98
const config = this.configurationService.getValue<IFilesConfiguration>();
99
100
return config?.files?.hotExit || HotExitConfiguration.ON_EXIT;
101
}
102
103
getEmptyWindowBackups(): IEmptyWindowBackupInfo[] {
104
return this.emptyWindows.slice(0); // return a copy
105
}
106
107
registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo): string;
108
registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom: string): Promise<string>;
109
registerWorkspaceBackup(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string | Promise<string> {
110
if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) {
111
this.workspaces.push(workspaceInfo);
112
this.storeWorkspacesMetadata();
113
}
114
115
const backupPath = join(this.backupHome, workspaceInfo.workspace.id);
116
117
if (migrateFrom) {
118
return this.moveBackupFolder(backupPath, migrateFrom).then(() => backupPath);
119
}
120
121
return backupPath;
122
}
123
124
private async moveBackupFolder(backupPath: string, moveFromPath: string): Promise<void> {
125
126
// Target exists: make sure to convert existing backups to empty window backups
127
if (await Promises.exists(backupPath)) {
128
await this.convertToEmptyWindowBackup(backupPath);
129
}
130
131
// When we have data to migrate from, move it over to the target location
132
if (await Promises.exists(moveFromPath)) {
133
try {
134
await Promises.rename(moveFromPath, backupPath, false /* no retry */);
135
} catch (error) {
136
this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`);
137
}
138
}
139
}
140
141
registerFolderBackup(folderInfo: IFolderBackupInfo): string {
142
if (!this.folders.some(folder => this.backupUriComparer.isEqual(folderInfo.folderUri, folder.folderUri))) {
143
this.folders.push(folderInfo);
144
this.storeWorkspacesMetadata();
145
}
146
147
return join(this.backupHome, this.getFolderHash(folderInfo));
148
}
149
150
registerEmptyWindowBackup(emptyWindowInfo: IEmptyWindowBackupInfo): string {
151
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, emptyWindowInfo.backupFolder))) {
152
this.emptyWindows.push(emptyWindowInfo);
153
this.storeWorkspacesMetadata();
154
}
155
156
return join(this.backupHome, emptyWindowInfo.backupFolder);
157
}
158
159
private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise<IWorkspaceBackupInfo[]> {
160
if (!Array.isArray(rootWorkspaces)) {
161
return [];
162
}
163
164
const seenIds: Set<string> = new Set();
165
const result: IWorkspaceBackupInfo[] = [];
166
167
// Validate Workspaces
168
for (const workspaceInfo of rootWorkspaces) {
169
const workspace = workspaceInfo.workspace;
170
if (!isWorkspaceIdentifier(workspace)) {
171
return []; // wrong format, skip all entries
172
}
173
174
if (!seenIds.has(workspace.id)) {
175
seenIds.add(workspace.id);
176
177
const backupPath = join(this.backupHome, workspace.id);
178
const hasBackups = await this.doHasBackups(backupPath);
179
180
// If the workspace has no backups, ignore it
181
if (hasBackups) {
182
if (workspace.configPath.scheme !== Schemas.file || await Promises.exists(workspace.configPath.fsPath)) {
183
result.push(workspaceInfo);
184
} else {
185
// If the workspace has backups, but the target workspace is missing, convert backups to empty ones
186
await this.convertToEmptyWindowBackup(backupPath);
187
}
188
} else {
189
await this.deleteStaleBackup(backupPath);
190
}
191
}
192
}
193
194
return result;
195
}
196
197
private async validateFolders(folderWorkspaces: IFolderBackupInfo[]): Promise<IFolderBackupInfo[]> {
198
if (!Array.isArray(folderWorkspaces)) {
199
return [];
200
}
201
202
const result: IFolderBackupInfo[] = [];
203
const seenIds: Set<string> = new Set();
204
for (const folderInfo of folderWorkspaces) {
205
const folderURI = folderInfo.folderUri;
206
const key = this.backupUriComparer.getComparisonKey(folderURI);
207
if (!seenIds.has(key)) {
208
seenIds.add(key);
209
210
const backupPath = join(this.backupHome, this.getFolderHash(folderInfo));
211
const hasBackups = await this.doHasBackups(backupPath);
212
213
// If the folder has no backups, ignore it
214
if (hasBackups) {
215
if (folderURI.scheme !== Schemas.file || await Promises.exists(folderURI.fsPath)) {
216
result.push(folderInfo);
217
} else {
218
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
219
await this.convertToEmptyWindowBackup(backupPath);
220
}
221
} else {
222
await this.deleteStaleBackup(backupPath);
223
}
224
}
225
}
226
227
return result;
228
}
229
230
private async validateEmptyWorkspaces(emptyWorkspaces: IEmptyWindowBackupInfo[]): Promise<IEmptyWindowBackupInfo[]> {
231
if (!Array.isArray(emptyWorkspaces)) {
232
return [];
233
}
234
235
const result: IEmptyWindowBackupInfo[] = [];
236
const seenIds: Set<string> = new Set();
237
238
// Validate Empty Windows
239
for (const backupInfo of emptyWorkspaces) {
240
const backupFolder = backupInfo.backupFolder;
241
if (typeof backupFolder !== 'string') {
242
return [];
243
}
244
245
if (!seenIds.has(backupFolder)) {
246
seenIds.add(backupFolder);
247
248
const backupPath = join(this.backupHome, backupFolder);
249
if (await this.doHasBackups(backupPath)) {
250
result.push(backupInfo);
251
} else {
252
await this.deleteStaleBackup(backupPath);
253
}
254
}
255
}
256
257
return result;
258
}
259
260
private async deleteStaleBackup(backupPath: string): Promise<void> {
261
try {
262
await Promises.rm(backupPath, RimRafMode.MOVE);
263
} catch (error) {
264
this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`);
265
}
266
}
267
268
private prepareNewEmptyWindowBackup(): IEmptyWindowBackupInfo {
269
270
// We are asked to prepare a new empty window backup folder.
271
// Empty windows backup folders are derived from a workspace
272
// identifier, so we generate a new empty workspace identifier
273
// until we found a unique one.
274
275
let emptyWorkspaceIdentifier = createEmptyWorkspaceIdentifier();
276
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, emptyWorkspaceIdentifier.id))) {
277
emptyWorkspaceIdentifier = createEmptyWorkspaceIdentifier();
278
}
279
280
return { backupFolder: emptyWorkspaceIdentifier.id };
281
}
282
283
private async convertToEmptyWindowBackup(backupPath: string): Promise<boolean> {
284
const newEmptyWindowBackupInfo = this.prepareNewEmptyWindowBackup();
285
286
// Rename backupPath to new empty window backup path
287
const newEmptyWindowBackupPath = join(this.backupHome, newEmptyWindowBackupInfo.backupFolder);
288
try {
289
await Promises.rename(backupPath, newEmptyWindowBackupPath, false /* no retry */);
290
} catch (error) {
291
this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`);
292
return false;
293
}
294
this.emptyWindows.push(newEmptyWindowBackupInfo);
295
296
return true;
297
}
298
299
async getDirtyWorkspaces(): Promise<Array<IWorkspaceBackupInfo | IFolderBackupInfo>> {
300
const dirtyWorkspaces: Array<IWorkspaceBackupInfo | IFolderBackupInfo> = [];
301
302
// Workspaces with backups
303
for (const workspace of this.workspaces) {
304
if ((await this.hasBackups(workspace))) {
305
dirtyWorkspaces.push(workspace);
306
}
307
}
308
309
// Folders with backups
310
for (const folder of this.folders) {
311
if ((await this.hasBackups(folder))) {
312
dirtyWorkspaces.push(folder);
313
}
314
}
315
316
return dirtyWorkspaces;
317
}
318
319
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | IFolderBackupInfo): Promise<boolean> {
320
let backupPath: string;
321
322
// Empty
323
if (isEmptyWindowBackupInfo(backupLocation)) {
324
backupPath = join(this.backupHome, backupLocation.backupFolder);
325
}
326
327
// Folder
328
else if (isFolderBackupInfo(backupLocation)) {
329
backupPath = join(this.backupHome, this.getFolderHash(backupLocation));
330
}
331
332
// Workspace
333
else {
334
backupPath = join(this.backupHome, backupLocation.workspace.id);
335
}
336
337
return this.doHasBackups(backupPath);
338
}
339
340
private async doHasBackups(backupPath: string): Promise<boolean> {
341
try {
342
const backupSchemas = await Promises.readdir(backupPath);
343
344
for (const backupSchema of backupSchemas) {
345
try {
346
const backupSchemaChildren = await Promises.readdir(join(backupPath, backupSchema));
347
if (backupSchemaChildren.length > 0) {
348
return true;
349
}
350
} catch (error) {
351
// invalid folder
352
}
353
}
354
} catch (error) {
355
// backup path does not exist
356
}
357
358
return false;
359
}
360
361
362
private storeWorkspacesMetadata(): void {
363
const serializedBackupWorkspaces: ISerializedBackupWorkspaces = {
364
workspaces: this.workspaces.map(({ workspace, remoteAuthority }) => {
365
const serializedWorkspaceBackupInfo: ISerializedWorkspaceBackupInfo = {
366
id: workspace.id,
367
configURIPath: workspace.configPath.toString()
368
};
369
370
if (remoteAuthority) {
371
serializedWorkspaceBackupInfo.remoteAuthority = remoteAuthority;
372
}
373
374
return serializedWorkspaceBackupInfo;
375
}),
376
folders: this.folders.map(({ folderUri, remoteAuthority }) => {
377
const serializedFolderBackupInfo: ISerializedFolderBackupInfo =
378
{
379
folderUri: folderUri.toString()
380
};
381
382
if (remoteAuthority) {
383
serializedFolderBackupInfo.remoteAuthority = remoteAuthority;
384
}
385
386
return serializedFolderBackupInfo;
387
}),
388
emptyWindows: this.emptyWindows.map(({ backupFolder, remoteAuthority }) => {
389
const serializedEmptyWindowBackupInfo: ISerializedEmptyWindowBackupInfo = {
390
backupFolder
391
};
392
393
if (remoteAuthority) {
394
serializedEmptyWindowBackupInfo.remoteAuthority = remoteAuthority;
395
}
396
397
return serializedEmptyWindowBackupInfo;
398
})
399
};
400
401
this.stateService.setItem(BackupMainService.backupWorkspacesMetadataStorageKey, serializedBackupWorkspaces);
402
}
403
404
protected getFolderHash(folder: IFolderBackupInfo): string {
405
const folderUri = folder.folderUri;
406
407
let key: string;
408
if (folderUri.scheme === Schemas.file) {
409
key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); // for backward compatibility, use the fspath as key
410
} else {
411
key = folderUri.toString().toLowerCase();
412
}
413
414
return createHash('md5').update(key).digest('hex'); // CodeQL [SM04514] Using MD5 to convert a file path to a fixed length
415
}
416
}
417
418