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;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionAddExchange;
import java.util.UUID;
import io.xpipe.core.util.ValidationException;
public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var cat = msg.getCategory() != null ? msg.getCategory() : DataStorage.DEFAULT_CATEGORY_UUID;
var entry = DataStorage.get().addStoreEntryIfNotPresent(DataStoreEntry.createNew(UUID.randomUUID(), cat, msg.getName(), msg.getData()));
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var found = DataStorage.get().getStoreEntryIfPresent(msg.getData(), false);
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();
}
}

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)) {
throw new BeaconClientException("Not a toggleable connection");
}
if (msg.isState()) {
if (msg.getState()) {
singletonSessionStore.startSessionIfNeeded();
} else {
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.AppVersion;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.beacon.api.DaemonVersionExchange;
import com.sun.net.httpserver.HttpExchange;
@ -19,6 +20,7 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
+ System.getProperty("java.vm.name") + " ("
+ System.getProperty("java.vm.version") + ")";
var version = AppProperties.get().getVersion();
var pro = LicenseProvider.get().hasPaidLicense();
return Response.builder()
.version(version)
.canonicalVersion(AppVersion.parse(version)
@ -26,6 +28,7 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
.orElse("?"))
.buildVersion(AppProperties.get().getBuild())
.jvmVersion(jvmVersion)
.pro(pro)
.build();
}
}

View file

@ -43,6 +43,7 @@ public class BrowserTransferComp extends SimpleComp {
var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true))
.visible(Bindings.isEmpty(syncItems));
var backgroundStack =
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")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> struc.get().setWrapText(true))
.hide(Bindings.isEmpty(syncItems));
var downloadButton = new IconButtonComp("mdi2d-download", () -> {

View file

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

View file

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

View file

@ -1307,6 +1307,164 @@ curl -X POST http://localhost:21721/connection/toggle \
</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
<a id="opIdshellStart"></a>
@ -1334,11 +1492,14 @@ These errors will be returned with the HTTP return code 500.
> Example responses
> 400 Response
> 200 Response
```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|
|---|---|---|---|
|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)|
|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|
@ -2621,6 +2782,32 @@ undefined
|---|---|---|---|---|
|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>
<a id="schemashellstoprequest"></a>
@ -2917,6 +3104,26 @@ undefined
|usageCategory|desktop|
|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>
<a id="schemaconnectionaddrequest"></a>
@ -2927,7 +3134,6 @@ undefined
```json
{
"name": "string",
"category": "string",
"data": {}
}
@ -2938,7 +3144,6 @@ undefined
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|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.|
<h2 id="tocS_ConnectionAddResponse">ConnectionAddResponse</h2>

View file

@ -72,7 +72,7 @@ public abstract class BeaconInterface<T> {
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();
}

View file

@ -26,7 +26,8 @@ public class ConnectionAddExchange extends BeaconInterface<ConnectionAddExchange
@NonNull
DataStore data;
UUID category;
@NonNull
Boolean validate;
}
@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
UUID connection;
boolean state;
@NonNull
Boolean state;
}
@Jacksonized

View file

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

View file

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

View file

@ -2,3 +2,4 @@
- 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/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'
'500':
$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:
post:
summary: Start shell connection
@ -305,6 +334,10 @@ paths:
responses:
'200':
description: The operation was successful. The shell session was started.
content:
application/json:
schema:
$ref: '#/components/schemas/ShellStartResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@ -549,6 +582,26 @@ components:
description: The connection uuid
required:
- 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:
type: object
properties:
@ -736,15 +789,20 @@ components:
- lastUsed
- lastModified
- state
ConnectionRefreshRequest:
type: object
properties:
connection:
type: string
description: The connection uuid
required:
- connection
ConnectionAddRequest:
type: object
properties:
name:
type: string
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:
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.

View file

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