diff --git a/README.md b/README.md index bac2117..b78f186 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A Java client library for interacting with the DatabunkerPro API. DatabunkerPro - Complete implementation of the DatabunkerPro API - User management (create, get, update, delete, patch) - App data management +- File storage (encrypted per-user files with tags and expiration) - Legal basis and agreement management - Connector management - Group and role management @@ -48,7 +49,7 @@ Add the repository and dependency to your `pom.xml`: org.databunker databunkerpro-java - 1.0.0-SNAPSHOT + 1.1.0 ``` @@ -67,11 +68,11 @@ Add the JitPack repository and dependency to your `pom.xml`: com.github.securitybunker databunkerpro-java - v1.0.0 + v1.1.0 ``` -**Note**: Replace `v1.0.0` with your desired version tag (e.g., `v1.0.1`, `v2.0.0`, etc.) +**Note**: Replace `v1.1.0` with your desired version tag (e.g., `v1.1.1`, `v2.0.0`, etc.) ### From Local Maven Repository @@ -90,12 +91,24 @@ Then add the dependency to your `pom.xml`: org.databunker databunkerpro-java - 1.0.0-SNAPSHOT + 1.1.0 ``` ## New Features in Latest Version +### New in 1.1.0 + +- **File API**: Store, retrieve, list, retag and delete encrypted per-user files + (`createFile`, `getFile`, `listUserFiles`, `replaceFileTags`, `deleteFile`), plus + `bulkListFilesByTag` for bulk lookups. Options are passed with the typed `FileOptions` + builder (mimetype, tags, `finaltime`, `slidingtime`). +- **Apache HttpClient 5**: Migrated off end-of-life HttpClient 4.x. If your project pins + HttpClient transitively, it now resolves `org.apache.httpcomponents.client5:httpclient5`. +- **Removed internal portal endpoints**: `preloginUser`, `loginUser`, `createCaptcha`, + `getUIConf` and `getTenantConf` were internal to the DatabunkerPro web portal and are + no longer part of the client. + ### Enhanced API Methods - **Wrapping Key Generation**: Generate wrapping keys from Shamir's Secret Sharing keys - **Typed Patch Operations**: Use structured `PatchOperation` objects for user updates @@ -175,6 +188,39 @@ api.createAppData("email", "user@example.com", "appname", data, null); Map appData = api.getAppData("email", "user@example.com", "appname", null); ``` +### File Storage + +```java +// Store a file. The content is passed base64-encoded. +String filedata = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get("passport.pdf"))); +FileOptions fileOptions = FileOptions.builder() + .mimetype("application/pdf") + .tags(Arrays.asList("kyc", "passport")) + .finaltime("365d") + .build(); +Map created = api.createFile("email", "user@example.com", "passport.pdf", filedata, fileOptions, null); +String fileuuid = (String) created.get("fileuuid"); + +// Get a file by uuid +Map file = api.getFile("email", "user@example.com", fileuuid, null); + +// Get a file by name (the newest match is returned) +Map byName = api.getFile("email", "user@example.com", null, "passport.pdf", false, null); + +// List the metadata of a user's files, optionally filtered by a single tag +Map allFiles = api.listUserFiles("email", "user@example.com", null); +Map kycFiles = api.listUserFiles("email", "user@example.com", "kyc", null); + +// Replace the complete tag set on a file +api.replaceFileTags("email", "user@example.com", fileuuid, Arrays.asList("kyc", "verified"), null); + +// Delete a file +api.deleteFile("email", "user@example.com", fileuuid, null); +``` + +Tags are lowercased and de-duplicated by the server, must match `^[a-z0-9][a-z0-9._-]{0,49}$`, +and at most 16 are kept per file. + ### System Configuration ```java @@ -267,8 +313,8 @@ JitPack automatically builds and publishes your GitHub repository as a Maven dep 1. **Create a Git tag** for your release: ```bash - git tag v1.0.0 - git push origin v1.0.0 + git tag v1.1.0 + git push origin v1.1.0 ``` 2. **JitPack automatically builds** and publishes the package diff --git a/pom.xml b/pom.xml index d1556e2..670f2dd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.databunker databunkerpro-java - 1.1.0-SNAPSHOT + 1.1.0 jar DatabunkerPro Java Client diff --git a/src/main/java/org/databunker/options/FileOptions.java b/src/main/java/org/databunker/options/FileOptions.java index 51680df..7128a4d 100644 --- a/src/main/java/org/databunker/options/FileOptions.java +++ b/src/main/java/org/databunker/options/FileOptions.java @@ -84,4 +84,12 @@ public FileOptions build() { return new FileOptions(this); } } + + /** + * Creates a new builder for FileOptions + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } } diff --git a/src/test/java/org/databunker/DatabunkerproApiTest.java b/src/test/java/org/databunker/DatabunkerproApiTest.java index ab97fb1..14ec70c 100644 --- a/src/test/java/org/databunker/DatabunkerproApiTest.java +++ b/src/test/java/org/databunker/DatabunkerproApiTest.java @@ -13,10 +13,15 @@ import org.junit.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Random; import org.databunker.options.BasicOptions; +import org.databunker.options.FileOptions; import org.databunker.options.SharedRecordOptions; import static org.junit.Assert.*; @@ -269,6 +274,89 @@ public void testSharedRecordManagement() throws IOException { System.out.println("Successfully retrieved shared record: " + recorduuid); } + @Test + public void testFileManagement() throws IOException { + System.out.println("\nTesting file management..."); + String email = "test" + random.nextInt(1000000) + "@example.com"; + Map userData = Map.of( + "email", email, + "name", "Test User " + random.nextInt(1000000), + "phone", String.valueOf(random.nextInt(1000000)) + ); + api.createUser(userData, null, null); + + // Store a file + String content = "file content " + random.nextInt(1000000); + String filedata = Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8)); + String filename = "notes" + random.nextInt(1000000) + ".txt"; + FileOptions options = FileOptions.builder() + .mimetype("text/plain") + .tags(Arrays.asList("kyc", "notes")) + .finaltime("1d") + .build(); + Map createResult = api.createFile("email", email, filename, filedata, options, null); + assertNotNull(createResult); + assertEquals("ok", createResult.get("status")); + assertNotNull(createResult.get("fileuuid")); + String fileuuid = (String) createResult.get("fileuuid"); + System.out.println("Successfully created file: " + fileuuid); + + // Get the file by uuid and verify the content round-trips + Map getResult = api.getFile("email", email, fileuuid, null); + assertNotNull(getResult); + assertEquals("ok", getResult.get("status")); + assertEquals(filename, getResult.get("filename")); + assertEquals("text/plain", getResult.get("mimetype")); + assertEquals(content, new String(Base64.getDecoder().decode((String) getResult.get("filedata")), + StandardCharsets.UTF_8)); + System.out.println("Successfully retrieved file by uuid: " + fileuuid); + + // Get the same file by name + Map byName = api.getFile("email", email, null, filename, false, null); + assertNotNull(byName); + assertEquals("ok", byName.get("status")); + assertEquals(fileuuid, byName.get("fileuuid")); + System.out.println("Successfully retrieved file by name: " + filename); + + // List all files of the user + Map listResult = api.listUserFiles("email", email, null); + assertNotNull(listResult); + assertEquals("ok", listResult.get("status")); + List> files = (List>) listResult.get("files"); + assertNotNull(files); + assertEquals(1, files.size()); + assertEquals(fileuuid, files.get(0).get("fileuuid")); + + // List files filtered by a tag, and confirm a foreign tag matches nothing + Map taggedResult = api.listUserFiles("email", email, "kyc", null); + assertEquals("ok", taggedResult.get("status")); + assertEquals(1, ((List>) taggedResult.get("files")).size()); + Map otherTagResult = api.listUserFiles("email", email, "invoice", null); + assertEquals("ok", otherTagResult.get("status")); + assertTrue(((List>) otherTagResult.get("files")).isEmpty()); + System.out.println("Successfully listed user files"); + + // Replace the tag set + Map retagResult = api.replaceFileTags("email", email, fileuuid, + Arrays.asList("kyc", "verified"), null); + assertNotNull(retagResult); + assertEquals("ok", retagResult.get("status")); + List tags = (List) retagResult.get("tags"); + assertNotNull(tags); + assertEquals(2, tags.size()); + assertTrue(tags.contains("verified")); + assertFalse(tags.contains("notes")); + System.out.println("Successfully replaced file tags: " + tags); + + // Delete the file + Map deleteResult = api.deleteFile("email", email, fileuuid, null); + assertNotNull(deleteResult); + assertEquals("ok", deleteResult.get("status")); + Map afterDelete = api.listUserFiles("email", email, null); + assertTrue(((List>) afterDelete.get("files")).isEmpty()); + System.out.println("Successfully deleted file: " + fileuuid); + } + @Test public void testDeleteUsersBulk() throws IOException { System.out.println("\nTesting bulk user deletion...");