From 3dcdd062ac25be15d78cbf26ad97278d74b2c8fc Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Tue, 7 Apr 2026 12:13:02 +0300 Subject: [PATCH] Add sandbox backup support Implement API XRPCs (create/list/restore), sandbox helpers, and the Backup record + parser. Update README and CHANGELOG, add tests, and bump version to 0.1.4-SNAPSHOT --- CHANGELOG.md | 8 ++++ README.md | 27 +++++++++++- build.clj | 2 +- src/pocketenv_io/api.clj | 30 ++++++++++++++ src/pocketenv_io/sandbox.clj | 46 +++++++++++++++++++++ test/pocketenv_io/pocketenv_test.clj | 61 ++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0550f8..af7812d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.4] - 2026-04-07 + +### Added +- Sandbox backup support: `sandbox/create-backup`, `sandbox/list-backups`, `sandbox/restore-backup` +- `api/create-backup`, `api/list-backups`, `api/restore-backup` XRPC calls +- `Backup` record with fields `:id`, `:directory`, `:description`, `:expires-at`, `:created-at` + ## [0.1.3] - 2026-04-05 ### Fixed @@ -46,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Token resolved from `POCKETENV_TOKEN` env var or `~/.pocketenv/token.json` - MIT License +[0.1.4]: https://github.com/pocketenv-io/pocketenv-clojure/releases/tag/v0.1.4 [0.1.3]: https://github.com/pocketenv-io/pocketenv-clojure/releases/tag/v0.1.3 [0.1.2]: https://github.com/pocketenv-io/pocketenv-clojure/releases/tag/v0.1.2 [0.1.1]: https://github.com/pocketenv-io/pocketenv-clojure/releases/tag/v0.1.1 diff --git a/README.md b/README.md index 6a7c864..80c0260 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Clojure SDK for the [Pocketenv](https://pocketenv.io) sandbox platform. Add to your `deps.edn`: ```clojure -{:deps io.pocketenv/pocketenv {:mvn/version "0.1.3-SNAPSHOT"}} +{:deps io.pocketenv/pocketenv {:mvn/version "0.1.4-SNAPSHOT"}} ``` ## Configuration @@ -235,6 +235,31 @@ All three functions return `:ok` on success or `{:error reason}` on failure and --- +## Backups + +Create a point-in-time backup of a sandbox directory, list existing backups, or restore one. + +```clojure +;; Create a backup of /workspace (optional :description and :ttl in seconds) +(-> (pocketenv/get-sandbox "my-box") + (sandbox/create-backup "/workspace" {:description "before deploy" :ttl 86400})) +;; => {:ok nil} + +;; List all backups for a sandbox +(-> (pocketenv/get-sandbox "my-box") + (sandbox/list-backups)) +;; => {:ok [#Backup{:id "bk-..." :directory "/workspace" :description "before deploy" +;; :expires-at "2026-04-08T..." :created-at "2026-04-07T..."}]} + +;; Restore a backup by its id (no sandbox instance needed) +(sandbox/restore-backup "bk-...") +;; => {:ok nil} +``` + +`Backup` fields: `:id`, `:directory`, `:description`, `:expires-at`, `:created-at`. + +--- + ## Actor / profile ```clojure diff --git a/build.clj b/build.clj index 5110570..d5eaac6 100644 --- a/build.clj +++ b/build.clj @@ -4,7 +4,7 @@ [deps-deploy.deps-deploy :as dd])) (def lib 'io.pocketenv/pocketenv) -(def version "0.1.3-SNAPSHOT") +(def version "0.1.4-SNAPSHOT") #_ ; alternatively, use MAJOR.MINOR.COMMITS: (def version (format "1.0.%s" (b/git-count-revs nil))) (def class-dir "target/classes") diff --git a/src/pocketenv_io/api.clj b/src/pocketenv_io/api.clj index eeccdbf..3f598c0 100644 --- a/src/pocketenv_io/api.clj +++ b/src/pocketenv_io/api.clj @@ -325,3 +325,33 @@ (client/post "/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey" {"id" sandbox-id "authKey" encrypted "redacted" redacted} opts))) + +;; --------------------------------------------------------------------------- +;; Backups +;; --------------------------------------------------------------------------- + +(defn create-backup + "Creates a backup of `directory` in the sandbox. Options: :description, :ttl, :token" + [sandbox-id directory opts] + (let [body (-> {"directory" directory} + (maybe-assoc "description" (:description opts)) + (maybe-assoc "ttl" (:ttl opts)))] + (client/post "/xrpc/io.pocketenv.sandbox.createBackup" + body + (assoc opts :params {"id" sandbox-id})))) + +(defn list-backups + "Lists all backups for a sandbox. Returns {:ok [Backup]}." + [sandbox-id opts] + (let [r (client/get "/xrpc/io.pocketenv.sandbox.getBackups" + (assoc opts :params {"id" sandbox-id}))] + (if-let [data (:ok r)] + {:ok (mapv types/backup-from-map (get data "backups"))} + r))) + +(defn restore-backup + "Restores a backup by its id." + [backup-id opts] + (client/post "/xrpc/io.pocketenv.sandbox.restoreBackup" + {"backupId" backup-id} + opts)) diff --git a/src/pocketenv_io/sandbox.clj b/src/pocketenv_io/sandbox.clj index 6d246f7..a5f9014 100644 --- a/src/pocketenv_io/sandbox.clj +++ b/src/pocketenv_io/sandbox.clj @@ -41,6 +41,9 @@ (defrecord TailscaleAuthKey [id auth-key created-at]) +(defrecord Backup + [id directory description expires-at created-at]) + ;; --------------------------------------------------------------------------- ;; Constructors from raw API maps ;; --------------------------------------------------------------------------- @@ -115,6 +118,14 @@ :auth-key (get m "authKey") :created-at (get m "createdAt")})) +(defn backup-from-map [m] + (map->Backup + {:id (get m "id") + :directory (get m "directory") + :description (get m "description") + :expires-at (get m "expiresAt") + :created-at (get m "createdAt")})) + ;; --------------------------------------------------------------------------- ;; Pipe-safety helper ;; --------------------------------------------------------------------------- @@ -376,3 +387,38 @@ (let [sb (unwrap sandbox-or-result) copy (requiring-resolve 'pocketenv-io.copy/to)] (copy (:id sb) dest-sandbox-id src-path dest-path opts)))) + +;; --------------------------------------------------------------------------- +;; Backups +;; --------------------------------------------------------------------------- + +(defn create-backup + "Creates a backup of `directory` inside the sandbox. + + Options: :description, :ttl, :token" + ([sandbox-or-result directory] (create-backup sandbox-or-result directory {})) + ([sandbox-or-result directory opts] + (let [sb (unwrap sandbox-or-result) + api (requiring-resolve 'pocketenv-io.api/create-backup)] + (api (:id sb) directory opts)))) + +(defn list-backups + "Lists all backups for the sandbox. + + Returns {:ok [Backup]} + + Options: :token" + ([sandbox-or-result] (list-backups sandbox-or-result {})) + ([sandbox-or-result opts] + (let [sb (unwrap sandbox-or-result) + api (requiring-resolve 'pocketenv-io.api/list-backups)] + (api (:id sb) opts)))) + +(defn restore-backup + "Restores a backup by its id. Does not require a sandbox instance. + + Options: :token" + ([backup-id] (restore-backup backup-id {})) + ([backup-id opts] + (let [api (requiring-resolve 'pocketenv-io.api/restore-backup)] + (api backup-id opts)))) diff --git a/test/pocketenv_io/pocketenv_test.clj b/test/pocketenv_io/pocketenv_test.clj index 0669546..1c391b6 100644 --- a/test/pocketenv_io/pocketenv_test.clj +++ b/test/pocketenv_io/pocketenv_test.clj @@ -201,3 +201,64 @@ :dest-path "/dest" :opts {:token "t"}} @calls)))))) + +;; --------------------------------------------------------------------------- +;; Backup +;; --------------------------------------------------------------------------- + +(deftest backup-from-map-test + (testing "parses backup map" + (let [b (sandbox/backup-from-map {"id" "bk-1" + "directory" "/workspace" + "description" "my backup" + "expiresAt" "2025-01-01" + "createdAt" "2024-01-01"})] + (is (instance? pocketenv_io.sandbox.Backup b)) + (is (= "bk-1" (:id b))) + (is (= "/workspace" (:directory b))) + (is (= "my backup" (:description b))) + (is (= "2025-01-01" (:expires-at b))) + (is (= "2024-01-01" (:created-at b))))) + + (testing "optional fields default to nil" + (let [b (sandbox/backup-from-map {"id" "bk-2" "directory" "/home" "createdAt" "2024-01-01"})] + (is (nil? (:description b))) + (is (nil? (:expires-at b)))))) + +(deftest sandbox-create-backup-test + (testing "create-backup delegates to api/create-backup with sandbox id" + (let [calls (atom nil) + sb (sandbox/map->Sandbox {:id "sb-123" :name "my-box"})] + (with-redefs [pocketenv-io.api/create-backup + (fn [sandbox-id directory opts] + (reset! calls {:sandbox-id sandbox-id + :directory directory + :opts opts}) + {:ok nil})] + (is (= {:ok nil} (sandbox/create-backup sb "/workspace" {:token "t" :description "snap"}))) + (is (= {:sandbox-id "sb-123" + :directory "/workspace" + :opts {:token "t" :description "snap"}} + @calls)))))) + +(deftest sandbox-list-backups-test + (testing "list-backups delegates to api/list-backups with sandbox id" + (let [calls (atom nil) + sb (sandbox/map->Sandbox {:id "sb-456" :name "my-box"}) + fake [(sandbox/map->Backup {:id "bk-1" :directory "/workspace"})]] + (with-redefs [pocketenv-io.api/list-backups + (fn [sandbox-id opts] + (reset! calls {:sandbox-id sandbox-id :opts opts}) + {:ok fake})] + (is (= {:ok fake} (sandbox/list-backups sb {:token "t"}))) + (is (= {:sandbox-id "sb-456" :opts {:token "t"}} @calls)))))) + +(deftest sandbox-restore-backup-test + (testing "restore-backup delegates to api/restore-backup with backup-id" + (let [calls (atom nil)] + (with-redefs [pocketenv-io.api/restore-backup + (fn [backup-id opts] + (reset! calls {:backup-id backup-id :opts opts}) + {:ok nil})] + (is (= {:ok nil} (sandbox/restore-backup "bk-99" {:token "t"}))) + (is (= {:backup-id "bk-99" :opts {:token "t"}} @calls)))))) -- 2.51.2