Fixes for the data dir, remove store ids, update api doc [stage]

This commit is contained in:
crschnick 2024-06-18 22:26:16 +00:00
parent 734fac9af6
commit 1f7174db86
7 changed files with 213 additions and 105 deletions

View file

@ -60,6 +60,7 @@ public class AppProperties {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
} }
} }
var referenceDir = Files.exists(appDir) ? appDir : Path.of(System.getProperty("user.dir"));
image = ModuleHelper.isImage(); image = ModuleHelper.isImage();
fullVersion = Optional.ofNullable(System.getProperty("io.xpipe.app.fullVersion")) fullVersion = Optional.ofNullable(System.getProperty("io.xpipe.app.fullVersion"))
@ -89,7 +90,7 @@ public class AppProperties {
.map(s -> { .map(s -> {
var p = Path.of(s); var p = Path.of(s);
if (!p.isAbsolute()) { if (!p.isAbsolute()) {
p = appDir.resolve(p); p = referenceDir.resolve(p);
} }
return p; return p;
}) })

View file

@ -1108,6 +1108,170 @@ string
</details> </details>
## Read the content of a remote file
<a id="opIdfsRead"></a>
`POST /fs/read`
Reads the entire content of a remote file through an active shell session.
> Body parameter
```json
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
"path": "/home/user/myfile.txt"
}
```
<h3 id="read-the-content-of-a-remote-file-parameters">Parameters</h3>
|Name|In|Type|Required|Description|
|---|---|---|---|---|
|body|body|[FsReadRequest](#schemafsreadrequest)|true|none|
> Example responses
> 200 Response
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="read-the-content-of-a-remote-file-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The file was read.|string|
|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|
|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|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": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
"path": "/home/user/myfile.txt"
}';
const headers = {
'Content-Type':'application/json',
'Accept':'application/octet-stream',
'Authorization':'Bearer {access-token}'
};
fetch('http://localhost:21723/fs/read',
{
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/octet-stream',
'Authorization': 'Bearer {access-token}'
}
data = """
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
"path": "/home/user/myfile.txt"
}
"""
r = requests.post('http://localhost:21723/fs/read', headers = headers, data = data)
print(r.json())
```
```java
var uri = URI.create("http://localhost:21723/fs/read");
var client = HttpClient.newHttpClient();
var request = HttpRequest
.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Accept", "application/octet-stream")
.header("Authorization", "Bearer {access-token}")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
"path": "/home/user/myfile.txt"
}
"""))
.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/octet-stream"},
"Authorization": []string{"Bearer {access-token}"},
}
data := bytes.NewBuffer([]byte{jsonReq})
req, err := http.NewRequest("POST", "http://localhost:21723/fs/read", data)
req.Header = headers
client := &http.Client{}
resp, err := client.Do(req)
// ...
}
```
```shell
# You can also use wget
curl -X POST http://localhost:21723/fs/read \
-H 'Content-Type: application/json' \ -H 'Accept: application/octet-stream' \ -H 'Authorization: Bearer {access-token}' \
--data '
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
"path": "/home/user/myfile.txt"
}
'
```
</details>
## Write a blob to a remote file ## Write a blob to a remote file
<a id="opIdfsWrite"></a> <a id="opIdfsWrite"></a>
@ -1579,6 +1743,28 @@ curl -X POST http://localhost:21723/fs/script \
|blob|string|true|none|The blob uuid| |blob|string|true|none|The blob uuid|
|path|string|true|none|The target filepath| |path|string|true|none|The target filepath|
<h2 id="tocS_FsReadRequest">FsReadRequest</h2>
<a id="schemafsreadrequest"></a>
<a id="schema_FsReadRequest"></a>
<a id="tocSfsreadrequest"></a>
<a id="tocsfsreadrequest"></a>
```json
{
"connection": "string",
"path": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|connection|string|true|none|The connection uuid|
|path|string|true|none|The target file path|
<h2 id="tocS_FsScriptRequest">FsScriptRequest</h2> <h2 id="tocS_FsScriptRequest">FsScriptRequest</h2>
<a id="schemafsscriptrequest"></a> <a id="schemafsscriptrequest"></a>

View file

@ -28,7 +28,8 @@ public class BeaconServer {
} }
private static List<String> toProcessCommand(String toExec) { private static List<String> toProcessCommand(String toExec) {
return OsType.getLocal().equals(OsType.WINDOWS) ? List.of("cmd", "/c", toExec) : List.of("sh", "-c", toExec); // Having the trailing space is very important to force cmd to not interpret surrounding spaces and removing them
return OsType.getLocal().equals(OsType.WINDOWS) ? List.of("cmd", "/c", toExec + " ") : List.of("sh", "-c", toExec);
} }
public static Process tryStartCustom() throws Exception { public static Process tryStartCustom() throws Exception {

View file

@ -1,86 +0,0 @@
package io.xpipe.core.store;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* Represents a reference to an XPipe data source.
* This reference consists out of a collection name and an entry name to allow for better organisation.
* <p>
* To allow for a simple usage of data source ids, the collection and entry names are trimmed and
* converted to lower case names when creating them.
* The two names are separated by a colon and are therefore not allowed to contain colons themselves.
* <p>
* A missing collection name indicates that the data source exists only temporarily.
*
* @see #fromString(String)
*/
@EqualsAndHashCode
@Getter
public class DataStoreId {
public static final char SEPARATOR = ':';
private final List<String> names;
public DataStoreId(List<String> names) {
this.names = names;
}
/**
* Creates a new data source id from a collection name and an entry name.
*
* @throws IllegalArgumentException if any name is not valid
*/
public static DataStoreId create(String... names) {
if (names == null) {
throw new IllegalArgumentException("Names are null");
}
if (Arrays.stream(names).anyMatch(s -> s == null)) {
throw new IllegalArgumentException("Name is null");
}
if (Arrays.stream(names).anyMatch(s -> s.contains("" + SEPARATOR))) {
throw new IllegalArgumentException("Separator character " + SEPARATOR + " is not allowed in the names");
}
if (Arrays.stream(names).anyMatch(s -> s.trim().length() == 0)) {
throw new IllegalArgumentException("Trimmed entry name is empty");
}
return new DataStoreId(Arrays.stream(names).toList());
}
/**
* Creates a new data source id from a string representation.
* The string must contain exactly one colon and non-empty names.
*
* @param s the string representation, must be not null and fulfill certain requirements
* @throws IllegalArgumentException if the string is not valid
*/
public static DataStoreId fromString(String s) {
if (s == null) {
throw new IllegalArgumentException("String is null");
}
var split = s.split(String.valueOf(SEPARATOR), -1);
var names =
Arrays.stream(split).map(String::trim).map(String::toLowerCase).toList();
if (names.stream().anyMatch(s1 -> s1.isEmpty())) {
throw new IllegalArgumentException("Name must not be empty");
}
return new DataStoreId(names);
}
@Override
public String toString() {
return names.stream().map(String::toLowerCase).collect(Collectors.joining("" + SEPARATOR));
}
}

View file

@ -1,59 +1,59 @@
package io.xpipe.core.test; package io.xpipe.core.test;
import io.xpipe.core.store.DataStoreId; import io.xpipe.core.store.StorePath;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
public class DataStoreIdTest { public class StorePathTest {
@Test @Test
public void testCreateInvalidParameters() { public void testCreateInvalidParameters() {
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("a:bc", "abc"); StorePath.create("a/bc", "abc");
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create(" \t", "abc"); StorePath.create(" \t", "abc");
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("", "abc"); StorePath.create("", "abc");
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("abc", null); StorePath.create("abc", null);
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("abc", "a:bc"); StorePath.create("abc", "a/bc");
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("abc", " \t"); StorePath.create("abc", " \t");
}); });
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.create("abc", ""); StorePath.create("abc", "");
}); });
} }
@Test @Test
public void testFromStringNullParameters() { public void testFromStringNullParameters() {
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.fromString(null); StorePath.fromString(null);
}); });
} }
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = {"abc:", "ab::c", "::abc", "::::", "", " "}) @ValueSource(strings = {"abc/", "ab//c", "//abc", "////", "", " "})
public void testFromStringInvalidParameters(String arg) { public void testFromStringInvalidParameters(String arg) {
Assertions.assertThrows(IllegalArgumentException.class, () -> { Assertions.assertThrows(IllegalArgumentException.class, () -> {
DataStoreId.fromString(arg); StorePath.fromString(arg);
}); });
} }
@Test @Test
public void testFromStringValidParameters() { public void testFromStringValidParameters() {
Assertions.assertEquals(DataStoreId.fromString("ab:c"), DataStoreId.fromString(" ab: c ")); Assertions.assertEquals(StorePath.fromString("ab/c"), StorePath.fromString(" ab/ c "));
Assertions.assertEquals(DataStoreId.fromString("ab:c"), DataStoreId.fromString(" AB: C ")); Assertions.assertEquals(StorePath.fromString("ab/c"), StorePath.fromString(" AB/ C "));
Assertions.assertEquals(DataStoreId.fromString("ab:c"), DataStoreId.fromString("ab:c ")); Assertions.assertEquals(StorePath.fromString("ab/c"), StorePath.fromString("ab/c "));
} }
} }

View file

@ -8,8 +8,12 @@ To start off, you can query connections based on various filters.
With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them.
You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs.
Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection.
You can also access the file systems via these shell connections to read and write remote files.
There will be more functionality added to the API in the future, for now this initial implementation is open for feedback. There will also be more functionality added to the API in the future.
There already exists a community made XPipe API client for python at https://github.com/coandco/python_xpipe_client.
It allows you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API.
## Service integration ## Service integration
@ -63,6 +67,8 @@ The UI has also been streamlined to make common actions and toggles more easily
- Support VMs for tunneling - Support VMs for tunneling
- Searching for connections has been improved to show children as well - Searching for connections has been improved to show children as well
- The welcome screen will now also contain the option to straight up jump to the synchronization settings - The welcome screen will now also contain the option to straight up jump to the synchronization settings
- You can now launch xpipe in another data directory with `xpipe open -d "<dir>"`
- Add option to use double clicks to open connections instead of single clicks
- Add support for foot terminal - Add support for foot terminal
- Fix elementary terminal not launching correctly - Fix elementary terminal not launching correctly
- Fix kubernetes not elevating correctly for non-default contexts - Fix kubernetes not elevating correctly for non-default contexts

View file

@ -1 +1 @@
10.0-11 10.0-12