mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-12 11:49:27 +00:00
feat: rename, deletion, and validation support
Closes #1599 Closes #1825 Closes #649 Closes #1847 Closes #1882 Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Co-authored-by: per1234 <accounts@perglass.com> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import * as createPaths from './create-paths';
|
||||
import { posix } from './create-paths';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import { Create, CreateError } from './typings';
|
||||
|
||||
export interface ResponseResultProvider {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(response: Response): Promise<any>;
|
||||
}
|
||||
export namespace ResponseResultProvider {
|
||||
@@ -15,6 +19,8 @@ export namespace ResponseResultProvider {
|
||||
export const JSON: ResponseResultProvider = (response) => response.json();
|
||||
}
|
||||
|
||||
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
|
||||
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
|
||||
export function Utf8ArrayToStr(array: Uint8Array): string {
|
||||
let out, i, c;
|
||||
let char2, char3;
|
||||
@@ -61,20 +67,13 @@ type ResourceType = 'f' | 'd';
|
||||
@injectable()
|
||||
export class CreateApi {
|
||||
@inject(SketchCache)
|
||||
protected sketchCache: SketchCache;
|
||||
|
||||
protected authenticationService: AuthenticationClientService;
|
||||
protected arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
public init(
|
||||
authenticationService: AuthenticationClientService,
|
||||
arduinoPreferences: ArduinoPreferences
|
||||
): CreateApi {
|
||||
this.authenticationService = authenticationService;
|
||||
this.arduinoPreferences = arduinoPreferences;
|
||||
|
||||
return this;
|
||||
}
|
||||
readonly sketchCache: SketchCache;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
@inject(SketchesService)
|
||||
private readonly sketchesService: SketchesService;
|
||||
|
||||
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
|
||||
return {
|
||||
@@ -129,10 +128,13 @@ export class CreateApi {
|
||||
|
||||
async createSketch(
|
||||
posixPath: string,
|
||||
content: string = CreateApi.defaultInoContent
|
||||
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent()
|
||||
): Promise<Create.Sketch> {
|
||||
const url = new URL(`${this.domain()}/sketches`);
|
||||
const headers = await this.headers();
|
||||
const [headers, content] = await Promise.all([
|
||||
this.headers(),
|
||||
contentProvider,
|
||||
]);
|
||||
const payload = {
|
||||
ino: btoa(content),
|
||||
path: posixPath,
|
||||
@@ -291,7 +293,7 @@ export class CreateApi {
|
||||
this.sketchCache.addSketch(sketch);
|
||||
|
||||
let file = '';
|
||||
if (sketch && sketch.secrets) {
|
||||
if (sketch.secrets) {
|
||||
for (const item of sketch.secrets) {
|
||||
file += `#define ${item.name} "${item.value}"\r\n`;
|
||||
}
|
||||
@@ -381,7 +383,7 @@ export class CreateApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not upload "do_not_sync" files/directoris and their descendants
|
||||
// do not upload "do_not_sync" files/directories and their descendants
|
||||
const segments = posixPath.split(posix.sep) || [];
|
||||
if (
|
||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||
@@ -415,6 +417,21 @@ export class CreateApi {
|
||||
await this.delete(posixPath, 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
|
||||
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
|
||||
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
|
||||
* instead of 404 when deleting a non-existing resource.
|
||||
*/
|
||||
async deleteSketch(sketchPath: string): Promise<void> {
|
||||
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
|
||||
const headers = await this.headers();
|
||||
await this.run(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
private async delete(posixPath: string, type: ResourceType): Promise<void> {
|
||||
const url = new URL(
|
||||
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
|
||||
@@ -475,14 +492,12 @@ export class CreateApi {
|
||||
}
|
||||
|
||||
private async run<T>(
|
||||
requestInfo: RequestInfo | URL,
|
||||
requestInfo: URL,
|
||||
init: RequestInit | undefined,
|
||||
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
||||
): Promise<T> {
|
||||
const response = await fetch(
|
||||
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
|
||||
init
|
||||
);
|
||||
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`);
|
||||
const response = await fetch(requestInfo.toString(), init);
|
||||
if (!response.ok) {
|
||||
let details: string | undefined = undefined;
|
||||
try {
|
||||
@@ -516,19 +531,3 @@ export class CreateApi {
|
||||
return this.authenticationService.session?.accessToken || '';
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateApi {
|
||||
export const defaultInoContent = `/*
|
||||
|
||||
*/
|
||||
|
||||
void setup() {
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
95
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
95
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Sketch } from '../../common/protocol';
|
||||
import { AuthenticationSession } from '../../node/auth/types';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
|
||||
@injectable()
|
||||
export class CreateFeatures implements FrontendApplicationContribution {
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
private readonly onDidChangeSessionEmitter = new Emitter<
|
||||
AuthenticationSession | undefined
|
||||
>();
|
||||
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeSessionEmitter,
|
||||
this.onDidChangeEnabledEmitter
|
||||
);
|
||||
private _enabled: boolean;
|
||||
private _session: AuthenticationSession | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.onDidChangeSessionEmitter.fire(this._session);
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.onDidChangeEnabledEmitter.fire(this._enabled);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
|
||||
return this.onDidChangeSessionEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeEnabled(): Event<boolean> {
|
||||
return this.onDidChangeEnabledEmitter.event;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
get session(): AuthenticationSession | undefined {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
|
||||
* Returns with `undefined` if `dataDirUri` is `undefined`.
|
||||
*/
|
||||
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
|
||||
if (!dataDirUri) {
|
||||
console.warn(
|
||||
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
|
||||
}
|
||||
|
||||
cloudUri(sketch: Sketch): URI | undefined {
|
||||
if (!this.session) {
|
||||
return undefined;
|
||||
}
|
||||
return this.localCacheFsProvider.from(new URI(sketch.uri));
|
||||
}
|
||||
}
|
||||
@@ -189,10 +189,6 @@ export class CreateFsProvider
|
||||
FileSystemProviderErrorCode.NoPermissions
|
||||
);
|
||||
}
|
||||
|
||||
return this.createApi.init(
|
||||
this.authenticationService,
|
||||
this.arduinoPreferences
|
||||
);
|
||||
return this.createApi;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,3 +71,23 @@ export class CreateError extends Error {
|
||||
Object.setPrototypeOf(this, CreateError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export type ConflictError = CreateError & { status: 409 };
|
||||
export function isConflict(err: unknown): err is ConflictError {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
|
||||
export type NotFoundError = CreateError & { status: 404 };
|
||||
export function isNotFound(err: unknown): err is NotFoundError {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is CreateError & { status: number } {
|
||||
if (err instanceof CreateError) {
|
||||
return err.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user