api rework [stage]

This commit is contained in:
crschnick 2024-07-04 12:08:58 +00:00
parent 83a616916e
commit c83b627307
17 changed files with 384 additions and 21 deletions

View file

@ -1,19 +1,39 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionAddExchange; import io.xpipe.beacon.api.ConnectionAddExchange;
import io.xpipe.core.util.ValidationException;
import java.util.UUID;
public class ConnectionAddExchangeImpl extends ConnectionAddExchange { public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override @Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var cat = msg.getCategory() != null ? msg.getCategory() : DataStorage.DEFAULT_CATEGORY_UUID; var found = DataStorage.get().getStoreEntryIfPresent(msg.getData(), false);
var entry = DataStorage.get().addStoreEntryIfNotPresent(DataStoreEntry.createNew(UUID.randomUUID(), cat, msg.getName(), msg.getData())); if (found.isPresent()) {
return Response.builder().connection(found.get().getUuid()).build();
}
var entry = DataStoreEntry.createNew(msg.getName(), msg.getData());
try {
DataStorage.get().addStoreEntryInProgress(entry);
if (msg.getValidate()) {
entry.validateOrThrow();
}
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
}
throw ex;
} finally {
DataStorage.get().removeStoreEntryInProgress(entry);
}
DataStorage.get().addStoreEntryIfNotPresent(entry);
return Response.builder().connection(entry.getUuid()).build(); return Response.builder().connection(entry.getUuid()).build();
} }
} }

View file

@ -0,0 +1,23 @@
package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRefreshExchange;
public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var e = DataStorage.get()
.getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (e.getStore() instanceof FixedHierarchyStore) {
DataStorage.get().refreshChildren(e, true);
} else {
e.validateOrThrow();
}
return Response.builder().build();
}
}

View file

@ -16,7 +16,7 @@ public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
if (!(e.getStore() instanceof SingletonSessionStore<?> singletonSessionStore)) { if (!(e.getStore() instanceof SingletonSessionStore<?> singletonSessionStore)) {
throw new BeaconClientException("Not a toggleable connection"); throw new BeaconClientException("Not a toggleable connection");
} }
if (msg.isState()) { if (msg.getState()) {
singletonSessionStore.startSessionIfNeeded(); singletonSessionStore.startSessionIfNeeded();
} else { } else {
singletonSessionStore.stopSessionIfNeeded(); singletonSessionStore.stopSessionIfNeeded();

View file

@ -2,6 +2,7 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppVersion; import io.xpipe.app.core.AppVersion;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.beacon.api.DaemonVersionExchange; import io.xpipe.beacon.api.DaemonVersionExchange;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
@ -19,6 +20,7 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
+ System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.name") + " ("
+ System.getProperty("java.vm.version") + ")"; + System.getProperty("java.vm.version") + ")";
var version = AppProperties.get().getVersion(); var version = AppProperties.get().getVersion();
var pro = LicenseProvider.get().hasPaidLicense();
return Response.builder() return Response.builder()
.version(version) .version(version)
.canonicalVersion(AppVersion.parse(version) .canonicalVersion(AppVersion.parse(version)
@ -26,6 +28,7 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
.orElse("?")) .orElse("?"))
.buildVersion(AppProperties.get().getBuild()) .buildVersion(AppProperties.get().getBuild())
.jvmVersion(jvmVersion) .jvmVersion(jvmVersion)
.pro(pro)
.build(); .build();
} }
} }

View file

@ -43,6 +43,7 @@ public class BrowserTransferComp extends SimpleComp {
var background = new LabelComp(AppI18n.observable("transferDescription")) var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true))
.visible(Bindings.isEmpty(syncItems)); .visible(Bindings.isEmpty(syncItems));
var backgroundStack = var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
@ -77,6 +78,7 @@ public class BrowserTransferComp extends SimpleComp {
aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> AppFont.medium(struc.get())) .apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> struc.get().setWrapText(true))
.hide(Bindings.isEmpty(syncItems)); .hide(Bindings.isEmpty(syncItems));
var downloadButton = new IconButtonComp("mdi2d-download", () -> { var downloadButton = new IconButtonComp("mdi2d-download", () -> {

View file

@ -16,6 +16,7 @@ import javafx.util.Pair;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -338,7 +339,12 @@ public abstract class DataStorage {
listeners.forEach(storageListener -> storageListener.onStoreListUpdate()); listeners.forEach(storageListener -> storageListener.onStoreListUpdate());
} }
@SneakyThrows
public boolean refreshChildren(DataStoreEntry e) { public boolean refreshChildren(DataStoreEntry e) {
return refreshChildren(e,false);
}
public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception {
if (!(e.getStore() instanceof FixedHierarchyStore)) { if (!(e.getStore() instanceof FixedHierarchyStore)) {
return false; return false;
} }
@ -348,8 +354,12 @@ public abstract class DataStorage {
try { try {
newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList(); newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList();
} catch (Exception ex) { } catch (Exception ex) {
if (throwOnFail) {
throw ex;
} else {
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
return false; return false;
}
} finally { } finally {
e.decrementBusyCounter(); e.decrementBusyCounter();
} }

View file

@ -139,6 +139,7 @@ open module io.xpipe.app {
ConnectionBrowseExchangeImpl, ConnectionBrowseExchangeImpl,
ConnectionTerminalExchangeImpl, ConnectionTerminalExchangeImpl,
ConnectionToggleExchangeImpl, ConnectionToggleExchangeImpl,
ConnectionRefreshExchangeImpl,
DaemonOpenExchangeImpl, DaemonOpenExchangeImpl,
DaemonFocusExchangeImpl, DaemonFocusExchangeImpl,
DaemonStatusExchangeImpl, DaemonStatusExchangeImpl,

View file

@ -1307,6 +1307,164 @@ curl -X POST http://localhost:21721/connection/toggle \
</details> </details>
## Refreshes state of a connection
<a id="opIdconnectionRefresh"></a>
`POST /connection/refresh`
Performs a refresh on the specified connection.
This will update the connection state information and also any children if the connection type has any.
> Body parameter
```json
{
"connection": "36ad9716-a209-4f7f-9814-078d3349280c"
}
```
<h3 id="refreshes-state-of-a-connection-parameters">Parameters</h3>
|Name|In|Type|Required|Description|
|---|---|---|---|---|
|body|body|[ConnectionRefreshRequest](#schemaconnectionrefreshrequest)|true|none|
> Example responses
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="refreshes-state-of-a-connection-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The request was successful. The connection state was updated.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None|
|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
bearerAuth
</aside>
<details>
<summary>Code samples</summary>
```javascript
const inputBody = '{
"connection": "36ad9716-a209-4f7f-9814-078d3349280c"
}';
const headers = {
'Content-Type':'application/json',
'Accept':'application/json',
'Authorization':'Bearer {access-token}'
};
fetch('http://localhost:21721/connection/refresh',
{
method: 'POST',
body: inputBody,
headers: headers
})
.then(function(res) {
return res.json();
}).then(function(body) {
console.log(body);
});
```
```python
import requests
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {access-token}'
}
data = """
{
"connection": "36ad9716-a209-4f7f-9814-078d3349280c"
}
"""
r = requests.post('http://localhost:21721/connection/refresh', headers = headers, data = data)
print(r.json())
```
```java
var uri = URI.create("http://localhost:21721/connection/refresh");
var client = HttpClient.newHttpClient();
var request = HttpRequest
.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer {access-token}")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"connection": "36ad9716-a209-4f7f-9814-078d3349280c"
}
"""))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
```
```go
package main
import (
"bytes"
"net/http"
)
func main() {
headers := map[string][]string{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
"Authorization": []string{"Bearer {access-token}"},
}
data := bytes.NewBuffer([]byte{jsonReq})
req, err := http.NewRequest("POST", "http://localhost:21721/connection/refresh", data)
req.Header = headers
client := &http.Client{}
resp, err := client.Do(req)
// ...
}
```
```shell
# You can also use wget
curl -X POST http://localhost:21721/connection/refresh \
-H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \
--data '
{
"connection": "36ad9716-a209-4f7f-9814-078d3349280c"
}
'
```
</details>
## Start shell connection ## Start shell connection
<a id="opIdshellStart"></a> <a id="opIdshellStart"></a>
@ -1334,11 +1492,14 @@ These errors will be returned with the HTTP return code 500.
> Example responses > Example responses
> 400 Response > 200 Response
```json ```json
{ {
"message": "string" "shellDialect": 0,
"osType": "string",
"osName": "string",
"temp": "string"
} }
``` ```
@ -1346,7 +1507,7 @@ These errors will be returned with the HTTP return code 500.
|Status|Meaning|Description|Schema| |Status|Meaning|Description|Schema|
|---|---|---|---| |---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was started.|None| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was started.|[ShellStartResponse](#schemashellstartresponse)|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None|
|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| |403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None|
@ -2621,6 +2782,32 @@ undefined
|---|---|---|---|---| |---|---|---|---|---|
|connection|string|true|none|The connection uuid| |connection|string|true|none|The connection uuid|
<h2 id="tocS_ShellStartResponse">ShellStartResponse</h2>
<a id="schemashellstartresponse"></a>
<a id="schema_ShellStartResponse"></a>
<a id="tocSshellstartresponse"></a>
<a id="tocsshellstartresponse"></a>
```json
{
"shellDialect": 0,
"osType": "string",
"osName": "string",
"temp": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|shellDialect|integer|true|none|The shell dialect|
|osType|string|true|none|The general type of operating system|
|osName|string|true|none|The display name of the operating system|
|temp|string|true|none|The location of the temporary directory|
<h2 id="tocS_ShellStopRequest">ShellStopRequest</h2> <h2 id="tocS_ShellStopRequest">ShellStopRequest</h2>
<a id="schemashellstoprequest"></a> <a id="schemashellstoprequest"></a>
@ -2917,6 +3104,26 @@ undefined
|usageCategory|desktop| |usageCategory|desktop|
|usageCategory|group| |usageCategory|group|
<h2 id="tocS_ConnectionRefreshRequest">ConnectionRefreshRequest</h2>
<a id="schemaconnectionrefreshrequest"></a>
<a id="schema_ConnectionRefreshRequest"></a>
<a id="tocSconnectionrefreshrequest"></a>
<a id="tocsconnectionrefreshrequest"></a>
```json
{
"connection": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|connection|string|true|none|The connection uuid|
<h2 id="tocS_ConnectionAddRequest">ConnectionAddRequest</h2> <h2 id="tocS_ConnectionAddRequest">ConnectionAddRequest</h2>
<a id="schemaconnectionaddrequest"></a> <a id="schemaconnectionaddrequest"></a>
@ -2927,7 +3134,6 @@ undefined
```json ```json
{ {
"name": "string", "name": "string",
"category": "string",
"data": {} "data": {}
} }
@ -2938,7 +3144,6 @@ undefined
|Name|Type|Required|Restrictions|Description| |Name|Type|Required|Restrictions|Description|
|---|---|---|---|---| |---|---|---|---|---|
|name|string|true|none|The connection name| |name|string|true|none|The connection name|
|category|string|false|none|The optional category uuid if you want to add the connection to a certain one. Otherwise the currently selected category will be used.|
|data|object|true|none|The raw connection store data. Schemas for connection types are not documented but you can find the connection data of your existing connections in the xpipe vault.| |data|object|true|none|The raw connection store data. Schemas for connection types are not documented but you can find the connection data of your existing connections in the xpipe vault.|
<h2 id="tocS_ConnectionAddResponse">ConnectionAddResponse</h2> <h2 id="tocS_ConnectionAddResponse">ConnectionAddResponse</h2>

View file

@ -72,7 +72,7 @@ public abstract class BeaconInterface<T> {
public abstract String getPath(); public abstract String getPath();
public Object handle(HttpExchange exchange, T body) throws Exception { public Object handle(HttpExchange exchange, T body) throws Throwable {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View file

@ -26,7 +26,8 @@ public class ConnectionAddExchange extends BeaconInterface<ConnectionAddExchange
@NonNull @NonNull
DataStore data; DataStore data;
UUID category; @NonNull
Boolean validate;
} }
@Jacksonized @Jacksonized

View file

@ -0,0 +1,30 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.UUID;
public class ConnectionRefreshExchange extends BeaconInterface<ConnectionRefreshExchange.Request> {
@Override
public String getPath() {
return "/connection/refresh";
}
@Jacksonized
@Builder
@Value
public static class Request {
@NonNull
UUID connection;
}
@Jacksonized
@Builder
@Value
public static class Response {}
}

View file

@ -22,7 +22,8 @@ public class ConnectionToggleExchange extends BeaconInterface<ConnectionToggleEx
@NonNull @NonNull
UUID connection; UUID connection;
boolean state; @NonNull
Boolean state;
} }
@Jacksonized @Jacksonized

View file

@ -3,6 +3,7 @@ package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface; import io.xpipe.beacon.BeaconInterface;
import lombok.Builder; import lombok.Builder;
import lombok.NonNull;
import lombok.Value; import lombok.Value;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@ -23,9 +24,15 @@ public class DaemonVersionExchange extends BeaconInterface<DaemonVersionExchange
@Value @Value
public static class Response { public static class Response {
@NonNull
String version; String version;
@NonNull
String canonicalVersion; String canonicalVersion;
@NonNull
String buildVersion; String buildVersion;
@NonNull
String jvmVersion; String jvmVersion;
@NonNull
Boolean pro;
} }
} }

View file

@ -42,6 +42,7 @@ open module io.xpipe.beacon {
ConnectionBrowseExchange, ConnectionBrowseExchange,
ConnectionTerminalExchange, ConnectionTerminalExchange,
ConnectionToggleExchange, ConnectionToggleExchange,
ConnectionRefreshExchange,
AskpassExchange, AskpassExchange,
TerminalWaitExchange, TerminalWaitExchange,
TerminalLaunchExchange, TerminalLaunchExchange,

View file

@ -2,3 +2,4 @@
- Add /connection/browse endpoint to open connections in the file browser - Add /connection/browse endpoint to open connections in the file browser
- Add /connection/terminal endpoint to open a terminal session four of connection - Add /connection/terminal endpoint to open a terminal session four of connection
- Add /connection/toggle endpoint to enable or disable connections such as tunnels and service forwards - Add /connection/toggle endpoint to enable or disable connections such as tunnels and service forwards
- Add /connection/refresh endpoint to refresh a connection state and its children

View file

@ -283,6 +283,35 @@ paths:
$ref: '#/components/responses/Forbidden' $ref: '#/components/responses/Forbidden'
'500': '500':
$ref: '#/components/responses/InternalServerError' $ref: '#/components/responses/InternalServerError'
/connection/refresh:
post:
summary: Refresh state of a connection
description: |
Performs a refresh on the specified connection.
This will update the connection state information and also any children if the connection type has any.
operationId: connectionRefresh
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ConnectionRefreshRequest'
examples:
simple:
summary: Refresh connection
value: { "connection": "36ad9716-a209-4f7f-9814-078d3349280c" }
responses:
'200':
description: The request was successful. The connection state was updated.
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
/shell/start: /shell/start:
post: post:
summary: Start shell connection summary: Start shell connection
@ -305,6 +334,10 @@ paths:
responses: responses:
'200': '200':
description: The operation was successful. The shell session was started. description: The operation was successful. The shell session was started.
content:
application/json:
schema:
$ref: '#/components/schemas/ShellStartResponse'
'400': '400':
$ref: '#/components/responses/BadRequest' $ref: '#/components/responses/BadRequest'
'401': '401':
@ -549,6 +582,26 @@ components:
description: The connection uuid description: The connection uuid
required: required:
- connection - connection
ShellStartResponse:
type: object
properties:
shellDialect:
type: integer
description: The shell dialect
osType:
type: string
description: The general type of operating system
osName:
type: string
description: The display name of the operating system
temp:
type: string
description: The location of the temporary directory
required:
- shellDialect
- osType
- osName
- temp
ShellStopRequest: ShellStopRequest:
type: object type: object
properties: properties:
@ -736,15 +789,20 @@ components:
- lastUsed - lastUsed
- lastModified - lastModified
- state - state
ConnectionRefreshRequest:
type: object
properties:
connection:
type: string
description: The connection uuid
required:
- connection
ConnectionAddRequest: ConnectionAddRequest:
type: object type: object
properties: properties:
name: name:
type: string type: string
description: The connection name description: The connection name
category:
type: string
description: The optional category uuid if you want to add the connection to a certain one. Otherwise the currently selected category will be used.
data: data:
type: object type: object
description: The raw connection store data. Schemas for connection types are not documented but you can find the connection data of your existing connections in the xpipe vault. description: The raw connection store data. Schemas for connection types are not documented but you can find the connection data of your existing connections in the xpipe vault.

View file

@ -1 +1 @@
10.1-1 10.1-2