From 1f7174db868b207fee863d9822b8e7354c590272 Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 18 Jun 2024 22:26:16 +0000 Subject: [PATCH] Fixes for the data dir, remove store ids, update api doc [stage] --- .../java/io/xpipe/app/core/AppProperties.java | 3 +- .../io/xpipe/app/resources/misc/api.md | 186 ++++++++++++++++++ .../java/io/xpipe/beacon/BeaconServer.java | 3 +- .../java/io/xpipe/core/store/DataStoreId.java | 86 -------- ...ataStoreIdTest.java => StorePathTest.java} | 30 +-- dist/changelogs/10.0.md | 8 +- version | 2 +- 7 files changed, 213 insertions(+), 105 deletions(-) delete mode 100644 core/src/main/java/io/xpipe/core/store/DataStoreId.java rename core/src/test/java/io/xpipe/core/test/{DataStoreIdTest.java => StorePathTest.java} (60%) diff --git a/app/src/main/java/io/xpipe/app/core/AppProperties.java b/app/src/main/java/io/xpipe/app/core/AppProperties.java index aa08eca4..8150cc87 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -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; }) diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md index 6363c135..6e975ee1 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -1108,6 +1108,170 @@ string +## Read the content of a remote file + + + +`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" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[FsReadRequest](#schemafsreadrequest)|true|none| + +> Example responses + +> 200 Response + +> 400 Response + +```json +{ + "message": "string" +} +``` + +

Responses

+ +|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)| + + + +
+ +Code samples + +```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" +} +' + +``` + +
+ ## Write a blob to a remote file @@ -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| +

FsReadRequest

+ + + + + + +```json +{ + "connection": "string", + "path": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| +|path|string|true|none|The target file path| +

FsScriptRequest

diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index bc086043..872bafc5 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -28,7 +28,8 @@ public class BeaconServer { } private static List 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 { diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreId.java b/core/src/main/java/io/xpipe/core/store/DataStoreId.java deleted file mode 100644 index 4c24f0b6..00000000 --- a/core/src/main/java/io/xpipe/core/store/DataStoreId.java +++ /dev/null @@ -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. - *

- * 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. - *

- * 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 names; - - public DataStoreId(List 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)); - } -} diff --git a/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java b/core/src/test/java/io/xpipe/core/test/StorePathTest.java similarity index 60% rename from core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java rename to core/src/test/java/io/xpipe/core/test/StorePathTest.java index e05468ef..39cc5daf 100644 --- a/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java +++ b/core/src/test/java/io/xpipe/core/test/StorePathTest.java @@ -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 ")); } } diff --git a/dist/changelogs/10.0.md b/dist/changelogs/10.0.md index 757ccdcf..2b8d4fef 100644 --- a/dist/changelogs/10.0.md +++ b/dist/changelogs/10.0.md @@ -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 "

"` +- 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 diff --git a/version b/version index 5c933c44..15fc8617 100644 --- a/version +++ b/version @@ -1 +1 @@ -10.0-11 +10.0-12