Skip to content

feat: add the general, plugin and storage commands - #41

Merged
marevol merged 5 commits into
feat/add-dict-commandsfrom
feat/add-general-plugin-storage-commands
Oct 16, 2026
Merged

marevol merged 5 commits into
feat/add-dict-commandsfrom
feat/add-general-plugin-storage-commands

Conversation

@marevol

@marevol marevol commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds three admin API wrappers: general (get, update), plugin (installed, available, install, uninstall) and storage (list, download, upload, delete). All commands support --output text|json|yaml and exit non-zero on an API failure in every format.

  • general update --set KEY=VALUE reads the current settings, then sends the four fields the server requires plus only the keys given. The server merges the request into the stored settings, so every other setting keeps its value. Unknown keys are rejected locally (the server accepts and ignores them). Integer settings are coerced from the type seen in get. Masked secrets are never sent back.
  • plugin uninstall needs a JSON body on DELETE, so send_request now sends json_data on DELETE through httpx.request (unchanged when no body is given). Install and uninstall are carried out by Fess in a background thread, so the commands say the request was accepted. uninstall asks for confirmation unless --yes.
  • storage takes plain object paths and computes the URL-safe Base64 id the API uses. download writes to --file (refuses to overwrite without --force) or to stdout. delete asks for confirmation unless --yes. Upload is a multipart POST.
  • Docs: skills/fessctl/references/features/{general,plugin,storage}.md, SKILL.md index and count (28), stale line in workflows.md.

Versions

API 14.19.2 15.6.1 15.7.0 15.8.0 15.9.0
general yes (update is POST) yes yes yes yes
plugin yes (install is PUT) yes yes yes yes
storage list/download/delete yes yes yes yes yes
storage upload yes (POST) no no no yes (POST)

On 15.6 to 15.8 the upload endpoint is declared as PUT only and the server does not parse a multipart PUT (file is required.; POST answers a 302), so storage upload reports that instead of sending a request. Before 15.9 an unreachable storage lists as an empty list; 15.9 returns Storage Access Error: ....

Verified live

Against Fess 15.9.0 (fess-opensearch 3.9.0), plus 15.8.0 and 14.19.2 for the version differences; the S3 backend was a LocalStack container.

  • general: full round trip on 15.9.0, changing one field changed exactly that field in get --output json (diff of 1 line) and reverting it gave a byte-identical 4107-byte file; also changing a string and an integer together, unknown key, non-integer, out-of-range value (server rejects, nothing changed), SPNEGO NTLM/Basic validation error, bad token (exit 3), secret handling. update also exercised on 15.8.0 and 14.19.2.
  • plugin: real install and uninstall of fess-ds-csv on 15.9.0 (jar appears and disappears in WEB-INF/plugin), unknown plugin/version, declined confirmation, repeat uninstall; installed/available on all three versions (41 available on 15.9.0). The 15 integration tests of this PR, run against stacks of 15.9.0, 15.8.0 and 14.19.2 through a stand-in for the compose fixture, passed on each (15/15).
  • storage on 15.9.0, 15.8.0 and 14.19.2 with an S3 backend: list (root, directory, nested), download to file and stdout (SHA-1 of a 300000-byte random file identical after upload and download), missing object, 0-byte object, overwrite refusal, delete, delete declined. Upload on 15.9.0 and 14.19.2; confirmed file is required. for PUT on 15.8.0.
  • storage without a backend: on 15.9.0 the unconfigured default (Access key ID cannot be blank), an unknown storage_type, and the real case with fess-storage-s3 uninstalled and Fess restarted (No storage client is registered as s3StorageClient ... install fess-storage-s3).
  • Unit tests: 234 pass (uv run pytest tests/unit); the integration tests collect.

Not verified

  • 15.6.1 and 15.7.0 were not run; their behaviour is taken from the source (same code as 15.8.0 for these APIs).
  • GCS and the S3 backend through anything but LocalStack.
  • Plugin install/uninstall on 14.x/15.x other than 15.9.0 (only the list calls were run there; the integration test installed fess-ds-csv successfully on 15.8.0 and 14.19.2 too).
  • tests/commands was not run through the repository's own compose fixture (it uses fixed ports/names).

Fess API oddities found

  • storage download of a missing object or unreachable storage returns HTTP 200 with Content-Length: 0; a real empty object is sent chunked. fessctl treats the former as an error.
  • Deleting a non-existent storage object succeeds; uninstalling a plugin that is not installed succeeds; plugin install/uninstall failures are only logged.
  • general accepts unknown keys silently; a non-integer in an integer field returns only error_code:<uuid>; unset LDAP keys are left out of the GET response.
  • The documented PUT /api/admin/storage/upload/{pathId}/ is a 405 on 15.8.0; the path is a form field.

Also verified against GCS (Fess 15.9.0 + fess-storage-gcs + fake-gcs-server): list, upload (small, 300000-byte, zero-byte, directories with spaces/unicode), download (SHA-1 identical to stdout and file), overwrite refusal, delete. This found and fixed three issues: upload --dir /a/b/ stored /a/b//file (Fess does not normalise the key), list hit the 5 second timeout when the backend is unreachable, and download --file into a missing directory crashed after fetching. Not verified: real GCS and the credentials-file mode.


Stacked PRs (merge from the bottom; each PR's base is the branch below it, so no conflicts remain):
main ← #45 theme timeout (+ CI runs for stacked PRs) ← #46 typer ← #43 ops commands ← #44 dict ← #41 general/plugin/storage ← #42 searchlist/documents.
After a PR is squash-merged GitHub retargets the next one to main.

Adds three admin API wrappers, available on 14.19.2, 15.6.1, 15.7.0,
15.8.0 and 15.9.0 (storage upload is not possible on 15.6 to 15.8, see below).

- general get / update: update reads the current settings, sends the four
  required fields plus only the keys given with --set KEY=VALUE, and relies
  on the server merging them into the stored settings, so other settings keep
  their values. Unknown keys are rejected locally because the server ignores
  them silently.
- plugin installed / available / install / uninstall: the uninstall request
  carries a JSON body on DELETE, so send_request now sends json_data on
  DELETE through httpx.request. Install and uninstall are carried out by Fess
  in a background thread; the command says the request was accepted.
- storage list / download / upload / delete: objects are addressed by the
  URL-safe Base64 of their path, computed by the client. Download writes to
  --file (refusing to overwrite without --force) or to stdout, and treats the
  200 with Content-Length 0 that Fess sends for an unreadable object as an
  error. Upload is a multipart POST (14.x, 15.9+); Fess 15.6 to 15.8 only
  declare PUT and do not parse it, so the command reports that instead.

All commands exit non-zero on an API failure in every output format.
Includes unit tests, integration tests and skills reference pages.
Fess queries the remote plugin repositories while serving
`plugin available`, which can exceed the 5 second default client
timeout (CI failed with "Network error: timed out" on 15.6.1). Use a
60 second timeout for that command.
…orage-commands

# Conflicts:
#	skills/fessctl/SKILL.md
#	skills/fessctl/references/workflows.md
#	src/fessctl/api/client.py
#	src/fessctl/cli.py
Verified against fake-gcs-server (Fess 15.9.0 with fess-storage-gcs):
- upload: strip leading and trailing slashes from --dir. Fess joins the
  directory and the name with a slash without normalising, so
  `-d /a/b/` stored `/a/b//file`, which `list` and `download` could
  not reach.
- list: use the long transfer timeout. Fess waits for the storage
  backend while serving the request, so an unreachable backend ended in
  a traceback after the 5 second default.
- download: check that the directory of --file exists before fetching
  the object, instead of crashing with FileNotFoundError after it.
@marevol
marevol added this pull request to stack #47 October 16, 2026 13:50
@marevol marevol added this to the 0.4.0 milestone Oct 16, 2026
@marevol marevol self-assigned this Oct 16, 2026
@marevol
marevol merged commit d9d1b86 into main Oct 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant