mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Fixes for the data dir, remove store ids, update api doc [stage]
This commit is contained in:
parent
734fac9af6
commit
1f7174db86
7 changed files with 213 additions and 105 deletions
|
@ -60,6 +60,7 @@ public class AppProperties {
|
|||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
}
|
||||
var referenceDir = Files.exists(appDir) ? appDir : Path.of(System.getProperty("user.dir"));
|
||||
|
||||
image = ModuleHelper.isImage();
|
||||
fullVersion = Optional.ofNullable(System.getProperty("io.xpipe.app.fullVersion"))
|
||||
|
@ -89,7 +90,7 @@ public class AppProperties {
|
|||
.map(s -> {
|
||||
var p = Path.of(s);
|
||||
if (!p.isAbsolute()) {
|
||||
p = appDir.resolve(p);
|
||||
p = referenceDir.resolve(p);
|
||||
}
|
||||
return p;
|
||||
})
|
||||
|
|
|
@ -1108,6 +1108,170 @@ string
|
|||
|
||||
</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
|
||||
|
||||
<a id="opIdfsWrite"></a>
|
||||
|
@ -1579,6 +1743,28 @@ curl -X POST http://localhost:21723/fs/script \
|
|||
|blob|string|true|none|The blob uuid|
|
||||
|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>
|
||||
|
||||
<a id="schemafsscriptrequest"></a>
|
||||
|
|
|
@ -28,7 +28,8 @@ public class BeaconServer {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -1,59 +1,59 @@
|
|||
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.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
public class DataStoreIdTest {
|
||||
public class StorePathTest {
|
||||
|
||||
@Test
|
||||
public void testCreateInvalidParameters() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("a:bc", "abc");
|
||||
StorePath.create("a/bc", "abc");
|
||||
});
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create(" \t", "abc");
|
||||
StorePath.create(" \t", "abc");
|
||||
});
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("", "abc");
|
||||
StorePath.create("", "abc");
|
||||
});
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("abc", null);
|
||||
StorePath.create("abc", null);
|
||||
});
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("abc", "a:bc");
|
||||
StorePath.create("abc", "a/bc");
|
||||
});
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("abc", " \t");
|
||||
StorePath.create("abc", " \t");
|
||||
});
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.create("abc", "");
|
||||
StorePath.create("abc", "");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromStringNullParameters() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.fromString(null);
|
||||
StorePath.fromString(null);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"abc:", "ab::c", "::abc", "::::", "", " "})
|
||||
@ValueSource(strings = {"abc/", "ab//c", "//abc", "////", "", " "})
|
||||
public void testFromStringInvalidParameters(String arg) {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
DataStoreId.fromString(arg);
|
||||
StorePath.fromString(arg);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromStringValidParameters() {
|
||||
Assertions.assertEquals(DataStoreId.fromString("ab:c"), DataStoreId.fromString(" ab: c "));
|
||||
Assertions.assertEquals(DataStoreId.fromString("ab:c"), DataStoreId.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(StorePath.fromString("ab/c"), StorePath.fromString(" AB/ C "));
|
||||
Assertions.assertEquals(StorePath.fromString("ab/c"), StorePath.fromString("ab/c "));
|
||||
}
|
||||
}
|
8
dist/changelogs/10.0.md
vendored
8
dist/changelogs/10.0.md
vendored
|
@ -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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
@ -63,6 +67,8 @@ The UI has also been streamlined to make common actions and toggles more easily
|
|||
- Support VMs for tunneling
|
||||
- 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
|
||||
- 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
|
||||
- Fix elementary terminal not launching correctly
|
||||
- Fix kubernetes not elevating correctly for non-default contexts
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
10.0-11
|
||||
10.0-12
|
||||
|
|
Loading…
Reference in a new issue