@@ -17,7 +17,6 @@ import {
17
17
PreferenceScope ,
18
18
} from '@theia/core/lib/browser/preferences/preference-service' ;
19
19
import { MessageService } from '@theia/core/lib/common/message-service' ;
20
- import { REMOTE_ONLY_FILES } from './../../create/create-fs-provider' ;
21
20
import { CreateApi } from '../../create/create-api' ;
22
21
import { CreateUri } from '../../create/create-uri' ;
23
22
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model' ;
@@ -33,10 +32,17 @@ import { ArduinoPreferences } from '../../arduino-preferences';
33
32
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl' ;
34
33
import { FileStat } from '@theia/filesystem/lib/common/files' ;
35
34
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree' ;
35
+ import { posix , splitSketchPath } from '../../create/create-paths' ;
36
+ import { Create } from '../../create/typings' ;
36
37
37
38
const MESSAGE_TIMEOUT = 5 * 1000 ;
38
39
const deepmerge = require ( 'deepmerge' ) . default ;
39
40
41
+ type FilesToWrite = { source : URI ; dest : URI } ;
42
+ type FilesToSync = {
43
+ filesToWrite : FilesToWrite [ ] ;
44
+ filesToDelete : URI [ ] ;
45
+ } ;
40
46
@injectable ( )
41
47
export class CloudSketchbookTree extends SketchbookTree {
42
48
@inject ( FileService )
@@ -94,7 +100,7 @@ export class CloudSketchbookTree extends SketchbookTree {
94
100
95
101
async pull ( arg : any ) : Promise < void > {
96
102
const {
97
- model,
103
+ // model,
98
104
node,
99
105
} : {
100
106
model : CloudSketchbookTreeModel ;
@@ -127,47 +133,12 @@ export class CloudSketchbookTree extends SketchbookTree {
127
133
const commandsCopy = node . commands ;
128
134
node . commands = [ ] ;
129
135
130
- // check if the sketch dir already exist
131
- if ( CloudSketchbookTree . CloudSketchTreeNode . isSynced ( node ) ) {
132
- const filesToPull = (
133
- await this . createApi . readDirectory ( node . remoteUri . path . toString ( ) )
134
- ) . filter ( ( file : any ) => ! REMOTE_ONLY_FILES . includes ( file . name ) ) ;
135
-
136
- await Promise . all (
137
- filesToPull . map ( ( file : any ) => {
138
- const uri = CreateUri . toUri ( file ) ;
139
- this . fileService . copy ( uri , LocalCacheUri . root . resolve ( uri . path ) , {
140
- overwrite : true ,
141
- } ) ;
142
- } )
143
- ) ;
136
+ const localUri = await this . fileService . toUnderlyingResource (
137
+ LocalCacheUri . root . resolve ( node . remoteUri . path )
138
+ ) ;
139
+ await this . sync ( node . remoteUri , localUri ) ;
144
140
145
- // open the pulled files in the current workspace
146
- const currentSketch = await this . sketchServiceClient . currentSketch ( ) ;
147
-
148
- if (
149
- ! CreateUri . is ( node . uri ) &&
150
- currentSketch &&
151
- currentSketch . uri === node . uri . toString ( )
152
- ) {
153
- filesToPull . forEach ( async ( file ) => {
154
- const localUri = LocalCacheUri . root . resolve (
155
- CreateUri . toUri ( file ) . path
156
- ) ;
157
- const underlying = await this . fileService . toUnderlyingResource (
158
- localUri
159
- ) ;
160
-
161
- model . open ( underlying ) ;
162
- } ) ;
163
- }
164
- } else {
165
- await this . fileService . copy (
166
- node . remoteUri ,
167
- LocalCacheUri . root . resolve ( node . uri . path ) ,
168
- { overwrite : true }
169
- ) ;
170
- }
141
+ this . sketchCache . purgeByPath ( node . remoteUri . path . toString ( ) ) ;
171
142
172
143
node . commands = commandsCopy ;
173
144
this . messageService . info ( `Done pulling ‘${ node . fileStat . name } ’.` , {
@@ -214,17 +185,107 @@ export class CloudSketchbookTree extends SketchbookTree {
214
185
}
215
186
const commandsCopy = node . commands ;
216
187
node . commands = [ ] ;
217
- // delete every first level file, then push everything
218
- const result = await this . fileService . copy ( node . uri , node . remoteUri , {
219
- overwrite : true ,
220
- } ) ;
188
+
189
+ const localUri = await this . fileService . toUnderlyingResource (
190
+ LocalCacheUri . root . resolve ( node . remoteUri . path )
191
+ ) ;
192
+ await this . sync ( localUri , node . remoteUri ) ;
193
+
194
+ this . sketchCache . purgeByPath ( node . remoteUri . path . toString ( ) ) ;
195
+
221
196
node . commands = commandsCopy ;
222
- this . messageService . info ( `Done pushing ‘${ result . name } ’.` , {
197
+ this . messageService . info ( `Done pushing ‘${ node . fileStat . name } ’.` , {
223
198
timeout : MESSAGE_TIMEOUT ,
224
199
} ) ;
225
200
} ) ;
226
201
}
227
202
203
+ async recursiveURIs ( uri : URI ) : Promise < URI [ ] > {
204
+ // remote resources can be fetched one-shot via api
205
+ if ( CreateUri . is ( uri ) ) {
206
+ const resources = await this . createApi . readDirectory (
207
+ uri . path . toString ( ) ,
208
+ { recursive : true , skipSketchCache : true }
209
+ ) ;
210
+ return resources . map ( ( resource ) =>
211
+ CreateUri . toUri ( splitSketchPath ( resource . path ) [ 1 ] )
212
+ ) ;
213
+ }
214
+
215
+ const fileStat = await this . fileService . resolve ( uri , {
216
+ resolveMetadata : false ,
217
+ } ) ;
218
+
219
+ if ( ! fileStat . children || ! fileStat . isDirectory ) {
220
+ return [ fileStat . resource ] ;
221
+ }
222
+
223
+ let childrenUris : URI [ ] = [ ] ;
224
+
225
+ for await ( const child of fileStat . children ) {
226
+ childrenUris = [
227
+ ...childrenUris ,
228
+ ...( await this . recursiveURIs ( child . resource ) ) ,
229
+ ] ;
230
+ }
231
+
232
+ return [ fileStat . resource , ...childrenUris ] ;
233
+ }
234
+
235
+ private URIsToMap ( uris : URI [ ] , basepath : string ) : Record < string , URI > {
236
+ return uris . reduce ( ( prev : Record < string , URI > , curr ) => {
237
+ const path = curr . toString ( ) . split ( basepath ) ;
238
+
239
+ if ( path . length !== 2 || path [ 1 ] . length === 0 ) {
240
+ return prev ;
241
+ }
242
+
243
+ // do not map "do_not_sync" files/directoris and their descendants
244
+ const segments = path [ 1 ] . split ( posix . sep ) || [ ] ;
245
+ if (
246
+ segments . some ( ( segment ) => Create . do_not_sync_files . includes ( segment ) )
247
+ ) {
248
+ return prev ;
249
+ }
250
+
251
+ // skip when the filename is a hidden file (starts with `.`)
252
+ if ( segments [ segments . length - 1 ] . indexOf ( '.' ) === 0 ) {
253
+ return prev ;
254
+ }
255
+
256
+ return { ...prev , [ path [ 1 ] ] : curr } ;
257
+ } , { } ) ;
258
+ }
259
+
260
+ async getUrisMap ( uri : URI ) {
261
+ const basepath = uri . toString ( ) ;
262
+ const exists = await this . fileService . exists ( uri ) ;
263
+ const uris =
264
+ ( exists && this . URIsToMap ( await this . recursiveURIs ( uri ) , basepath ) ) || { } ;
265
+ return uris ;
266
+ }
267
+
268
+ async treeDiff ( source : URI , dest : URI ) : Promise < FilesToSync > {
269
+ const [ sourceURIs , destURIs ] = await Promise . all ( [
270
+ this . getUrisMap ( source ) ,
271
+ this . getUrisMap ( dest ) ,
272
+ ] ) ;
273
+
274
+ const destBase = dest . toString ( ) ;
275
+ const filesToWrite : FilesToWrite [ ] = [ ] ;
276
+
277
+ Object . keys ( sourceURIs ) . forEach ( ( path ) => {
278
+ const destUri = destURIs [ path ] || new URI ( destBase + path ) ;
279
+
280
+ filesToWrite . push ( { source : sourceURIs [ path ] , dest : destUri } ) ;
281
+ delete destURIs [ path ] ;
282
+ } ) ;
283
+
284
+ const filesToDelete = Object . values ( destURIs ) ;
285
+
286
+ return { filesToWrite, filesToDelete } ;
287
+ }
288
+
228
289
async refresh (
229
290
node ?: CompositeTreeNode
230
291
) : Promise < CompositeTreeNode | undefined > {
@@ -266,6 +327,25 @@ export class CloudSketchbookTree extends SketchbookTree {
266
327
}
267
328
}
268
329
330
+ async sync ( source : URI , dest : URI ) {
331
+ const { filesToWrite, filesToDelete } = await this . treeDiff ( source , dest ) ;
332
+ await Promise . all (
333
+ filesToWrite . map ( async ( { source, dest } ) => {
334
+ if ( ( await this . fileService . resolve ( source ) ) . isFile ) {
335
+ const content = await this . fileService . read ( source ) ;
336
+ return this . fileService . write ( dest , content . value ) ;
337
+ }
338
+ return this . fileService . createFolder ( dest ) ;
339
+ } )
340
+ ) ;
341
+
342
+ await Promise . all (
343
+ filesToDelete . map ( ( file ) =>
344
+ this . fileService . delete ( file , { recursive : true } )
345
+ )
346
+ ) ;
347
+ }
348
+
269
349
async resolveChildren ( parent : CompositeTreeNode ) : Promise < TreeNode [ ] > {
270
350
return ( await super . resolveChildren ( parent ) ) . sort ( ( a , b ) => {
271
351
if (
@@ -295,7 +375,7 @@ export class CloudSketchbookTree extends SketchbookTree {
295
375
296
376
/**
297
377
* Retrieve fileStats for the given node, merging the local and remote childrens
298
- * Local children take prevedence over remote ones
378
+ * Local children take precedence over remote ones
299
379
* @param node
300
380
* @returns
301
381
*/
@@ -376,6 +456,7 @@ export class CloudSketchbookTree extends SketchbookTree {
376
456
const node = this . getNode ( id ) ;
377
457
if ( fileStat . isDirectory ) {
378
458
if ( DirNode . is ( node ) ) {
459
+ node . uri = uri ;
379
460
node . fileStat = fileStat ;
380
461
return node ;
381
462
}
@@ -391,6 +472,7 @@ export class CloudSketchbookTree extends SketchbookTree {
391
472
}
392
473
if ( FileNode . is ( node ) ) {
393
474
node . fileStat = fileStat ;
475
+ node . uri = uri ;
394
476
return node ;
395
477
}
396
478
return < FileNode > {
0 commit comments