Skip to content

Commit d790266

Browse files
authored
Improve remote sketchbook explorer (#459)
* Refactor remote sketchbook explorer * sketches sorting
1 parent 4da5d57 commit d790266

16 files changed

+593
-614
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ import { CloudSketchbookCompositeWidget } from './widgets/cloud-sketchbook/cloud
236236
import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
237237
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
238238
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
239+
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
239240

240241
const ElementQueries = require('css-element-queries/src/ElementQueries');
241242

@@ -686,6 +687,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
686687
createCloudSketchbookTreeWidget(container)
687688
);
688689
bind(CreateApi).toSelf().inSingletonScope();
690+
bind(SketchCache).toSelf().inSingletonScope();
691+
689692
bind(ShareSketchDialog).toSelf().inSingletonScope();
690693
bind(AuthenticationClientService).toSelf().inSingletonScope();
691694
bind(CommandContribution).toService(AuthenticationClientService);

Diff for: arduino-ide-extension/src/browser/create/create-api.ts

+60-179
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { injectable } from 'inversify';
1+
import { injectable, inject } from 'inversify';
22
import * as createPaths from './create-paths';
3-
import { posix, splitSketchPath } from './create-paths';
3+
import { posix } from './create-paths';
44
import { AuthenticationClientService } from '../auth/authentication-client-service';
55
import { ArduinoPreferences } from '../arduino-preferences';
6+
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
7+
import { Create, CreateError } from './typings';
68

79
export interface ResponseResultProvider {
810
(response: Response): Promise<any>;
@@ -15,10 +17,11 @@ export namespace ResponseResultProvider {
1517

1618
type ResourceType = 'f' | 'd';
1719

18-
export let sketchCache: Create.Sketch[] = [];
19-
2020
@injectable()
2121
export class CreateApi {
22+
@inject(SketchCache)
23+
protected sketchCache: SketchCache;
24+
2225
protected authenticationService: AuthenticationClientService;
2326
protected arduinoPreferences: ArduinoPreferences;
2427

@@ -32,48 +35,20 @@ export class CreateApi {
3235
return this;
3336
}
3437

35-
public sketchCompareByPath = (param: string) => {
36-
return (sketch: Create.Sketch) => {
37-
const [, spath] = splitSketchPath(sketch.path);
38-
return param === spath;
39-
};
40-
};
41-
42-
async findSketchInCache(
43-
compareFn: (sketch: Create.Sketch) => boolean,
44-
trustCache = true
45-
): Promise<Create.Sketch | undefined> {
46-
const sketch = sketchCache.find((sketch) => compareFn(sketch));
47-
if (trustCache) {
48-
return Promise.resolve(sketch);
49-
}
50-
return await this.sketch({ id: sketch?.id });
51-
}
52-
5338
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
5439
return {
5540
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
5641
modified_at: sketch.modified_at,
42+
created_at: sketch.created_at,
5743
name: `${Create.arduino_secrets_file}`,
5844
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
5945
mimetype: 'text/x-c++src; charset=utf-8',
6046
type: 'file',
61-
sketchId: sketch.id,
6247
};
6348
}
6449

65-
async sketch(opt: {
66-
id?: string;
67-
path?: string;
68-
}): Promise<Create.Sketch | undefined> {
69-
let url;
70-
if (opt.id) {
71-
url = new URL(`${this.domain()}/sketches/byID/${opt.id}`);
72-
} else if (opt.path) {
73-
url = new URL(`${this.domain()}/sketches/byPath${opt.path}`);
74-
} else {
75-
return;
76-
}
50+
async sketch(id: string): Promise<Create.Sketch> {
51+
const url = new URL(`${this.domain()}/sketches/byID/${id}`);
7752

7853
url.searchParams.set('user_id', 'me');
7954
const headers = await this.headers();
@@ -92,7 +67,7 @@ export class CreateApi {
9267
method: 'GET',
9368
headers,
9469
});
95-
sketchCache = result.sketches;
70+
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
9671
return result.sketches;
9772
}
9873

@@ -118,7 +93,7 @@ export class CreateApi {
11893

11994
async readDirectory(
12095
posixPath: string,
121-
options: { recursive?: boolean; match?: string; secrets?: boolean } = {}
96+
options: { recursive?: boolean; match?: string } = {}
12297
): Promise<Create.Resource[]> {
12398
const url = new URL(
12499
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
@@ -131,58 +106,21 @@ export class CreateApi {
131106
}
132107
const headers = await this.headers();
133108

134-
const sketchProm = options.secrets
135-
? this.sketches()
136-
: Promise.resolve(sketchCache);
137-
138-
return Promise.all([
139-
this.run<Create.RawResource[]>(url, {
140-
method: 'GET',
141-
headers,
142-
}),
143-
sketchProm,
144-
])
145-
.then(async ([result, sketches]) => {
146-
if (options.secrets) {
147-
// for every sketch with secrets, create a fake arduino_secrets.h
148-
result.forEach(async (res) => {
149-
if (res.type !== 'sketch') {
150-
return;
151-
}
152-
153-
const [, spath] = createPaths.splitSketchPath(res.path);
154-
const sketch = await this.findSketchInCache(
155-
this.sketchCompareByPath(spath)
156-
);
157-
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
158-
result.push(this.getSketchSecretStat(sketch));
159-
}
160-
});
161-
162-
if (posixPath !== posix.sep) {
163-
const sketch = await this.findSketchInCache(
164-
this.sketchCompareByPath(posixPath)
165-
);
166-
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
167-
result.push(this.getSketchSecretStat(sketch));
168-
}
109+
return this.run<Create.RawResource[]>(url, {
110+
method: 'GET',
111+
headers,
112+
})
113+
.then(async (result) => {
114+
// add arduino_secrets.h to the results, when reading a sketch main folder
115+
if (posixPath.length && posixPath !== posix.sep) {
116+
const sketch = this.sketchCache.getSketch(posixPath);
117+
118+
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
119+
result.push(this.getSketchSecretStat(sketch));
169120
}
170121
}
171-
const sketchesMap: Record<string, Create.Sketch> = sketches.reduce(
172-
(prev, curr) => {
173-
return { ...prev, [curr.path]: curr };
174-
},
175-
{}
176-
);
177122

178-
// add the sketch id and isPublic to the resource
179-
return result.map((resource) => {
180-
return {
181-
...resource,
182-
sketchId: sketchesMap[resource.path]?.id || '',
183-
isPublic: sketchesMap[resource.path]?.is_public || false,
184-
};
185-
});
123+
return result;
186124
})
187125
.catch((reason) => {
188126
if (reason?.status === 404) return [] as Create.Resource[];
@@ -214,18 +152,16 @@ export class CreateApi {
214152

215153
let resources;
216154
if (basename === Create.arduino_secrets_file) {
217-
const sketch = await this.findSketchInCache(
218-
this.sketchCompareByPath(parentPosixPath)
219-
);
155+
const sketch = this.sketchCache.getSketch(parentPosixPath);
220156
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
221157
} else {
222158
resources = await this.readDirectory(parentPosixPath, {
223159
match: basename,
224160
});
225161
}
226-
227-
resources.sort((left, right) => left.path.length - right.path.length);
228-
const resource = resources.find(({ name }) => name === basename);
162+
const resource = resources.find(
163+
({ path }) => createPaths.splitSketchPath(path)[1] === posixPath
164+
);
229165
if (!resource) {
230166
throw new CreateError(`Not found: ${posixPath}.`, 404);
231167
}
@@ -248,10 +184,7 @@ export class CreateApi {
248184
return data;
249185
}
250186

251-
const sketch = await this.findSketchInCache((sketch) => {
252-
const [, spath] = splitSketchPath(sketch.path);
253-
return spath === createPaths.parentPosix(path);
254-
}, true);
187+
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
255188

256189
if (
257190
sketch &&
@@ -273,14 +206,25 @@ export class CreateApi {
273206

274207
if (basename === Create.arduino_secrets_file) {
275208
const parentPosixPath = createPaths.parentPosix(posixPath);
276-
const sketch = await this.findSketchInCache(
277-
this.sketchCompareByPath(parentPosixPath),
278-
false
279-
);
209+
210+
//retrieve the sketch id from the cache
211+
const cacheSketch = this.sketchCache.getSketch(parentPosixPath);
212+
if (!cacheSketch) {
213+
throw new Error(`Unable to find sketch ${parentPosixPath} in cache`);
214+
}
215+
216+
// get a fresh copy of the sketch in order to guarantee fresh secrets
217+
const sketch = await this.sketch(cacheSketch.id);
218+
if (!sketch) {
219+
throw new Error(
220+
`Unable to get a fresh copy of the sketch ${cacheSketch.id}`
221+
);
222+
}
223+
this.sketchCache.addSketch(sketch);
280224

281225
let file = '';
282226
if (sketch && sketch.secrets) {
283-
for (const item of sketch?.secrets) {
227+
for (const item of sketch.secrets) {
284228
file += `#define ${item.name} "${item.value}"\r\n`;
285229
}
286230
}
@@ -310,9 +254,9 @@ export class CreateApi {
310254

311255
if (basename === Create.arduino_secrets_file) {
312256
const parentPosixPath = createPaths.parentPosix(posixPath);
313-
const sketch = await this.findSketchInCache(
314-
this.sketchCompareByPath(parentPosixPath)
315-
);
257+
258+
const sketch = this.sketchCache.getSketch(parentPosixPath);
259+
316260
if (sketch) {
317261
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
318262
const headers = await this.headers();
@@ -356,9 +300,10 @@ export class CreateApi {
356300
secrets: { data: secrets },
357301
};
358302

359-
// replace the sketch in the cache, so other calls will not overwrite each other
360-
sketchCache = sketchCache.filter((skt) => skt.id !== sketch.id);
361-
sketchCache.push({ ...sketch, secrets });
303+
// replace the sketch in the cache with the one we are pushing
304+
// TODO: we should do a get after the POST, in order to be sure the cache
305+
// is updated the most recent metadata
306+
this.sketchCache.addSketch(sketch);
362307

363308
const init = {
364309
method: 'POST',
@@ -370,6 +315,14 @@ export class CreateApi {
370315
return;
371316
}
372317

318+
// do not upload "do_not_sync" files/directoris and their descendants
319+
const segments = posixPath.split(posix.sep) || [];
320+
if (
321+
segments.some((segment) => Create.do_not_sync_files.includes(segment))
322+
) {
323+
return;
324+
}
325+
373326
const url = new URL(
374327
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
375328
);
@@ -512,75 +465,3 @@ void loop() {
512465
513466
`;
514467
}
515-
516-
export namespace Create {
517-
export interface Sketch {
518-
readonly name: string;
519-
readonly path: string;
520-
readonly modified_at: string;
521-
readonly created_at: string;
522-
523-
readonly secrets?: { name: string; value: string }[];
524-
525-
readonly id: string;
526-
readonly is_public: boolean;
527-
// readonly board_fqbn: '',
528-
// readonly board_name: '',
529-
// readonly board_type: 'serial' | 'network' | 'cloud' | '',
530-
readonly href?: string;
531-
readonly libraries: string[];
532-
// readonly tutorials: string[] | null;
533-
// readonly types: string[] | null;
534-
// readonly user_id: string;
535-
}
536-
537-
export type ResourceType = 'sketch' | 'folder' | 'file';
538-
export const arduino_secrets_file = 'arduino_secrets.h';
539-
export interface Resource {
540-
readonly name: string;
541-
/**
542-
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
543-
*/
544-
readonly path: string;
545-
readonly type: ResourceType;
546-
readonly sketchId: string;
547-
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
548-
readonly children?: number; // For 'sketch' and 'folder' types.
549-
readonly size?: number; // For 'sketch' type only.
550-
readonly isPublic?: boolean; // For 'sketch' type only.
551-
552-
readonly mimetype?: string; // For 'file' type.
553-
readonly href?: string;
554-
}
555-
export namespace Resource {
556-
export function is(arg: any): arg is Resource {
557-
return (
558-
!!arg &&
559-
'name' in arg &&
560-
typeof arg['name'] === 'string' &&
561-
'path' in arg &&
562-
typeof arg['path'] === 'string' &&
563-
'type' in arg &&
564-
typeof arg['type'] === 'string' &&
565-
'modified_at' in arg &&
566-
typeof arg['modified_at'] === 'string' &&
567-
(arg['type'] === 'sketch' ||
568-
arg['type'] === 'folder' ||
569-
arg['type'] === 'file')
570-
);
571-
}
572-
}
573-
574-
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
575-
}
576-
577-
export class CreateError extends Error {
578-
constructor(
579-
message: string,
580-
readonly status: number,
581-
readonly details?: string
582-
) {
583-
super(message);
584-
Object.setPrototypeOf(this, CreateError.prototype);
585-
}
586-
}

Diff for: arduino-ide-extension/src/browser/create/create-fs-provider.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ import {
2424
FileServiceContribution,
2525
} from '@theia/filesystem/lib/browser/file-service';
2626
import { AuthenticationClientService } from '../auth/authentication-client-service';
27-
import { Create, CreateApi } from './create-api';
27+
import { CreateApi } from './create-api';
2828
import { CreateUri } from './create-uri';
2929
import { SketchesService } from '../../common/protocol';
3030
import { ArduinoPreferences } from '../arduino-preferences';
31+
import { Create } from './typings';
3132

3233
export const REMOTE_ONLY_FILES = ['sketch.json'];
3334

@@ -106,10 +107,7 @@ export class CreateFsProvider
106107

107108
async readdir(uri: URI): Promise<[string, FileType][]> {
108109
const resources = await this.getCreateApi.readDirectory(
109-
uri.path.toString(),
110-
{
111-
secrets: true,
112-
}
110+
uri.path.toString()
113111
);
114112
return resources
115113
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name))

0 commit comments

Comments
 (0)