diff --git a/.env.example b/.env.example index bfcff31..d5f5b5e 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,10 @@ ONLYOFFICE_PROJECT_ID=33 # MINIO_BUCKET=office # MINIO_ACCESS_KEY= # MINIO_SECRET_KEY= + +# oo search — direct Elasticsearch access for name + content search. ES lives +# inside the OnlyOffice VM on localhost:9200; expose it with an SSH tunnel +# (see docs/elasticsearch.md). ONLYOFFICE_ES_INDEX defaults to files_file. +# ONLYOFFICE_ES_URL=http://127.0.0.1:9200 +# ONLYOFFICE_ES_INDEX=files_file +# ONLYOFFICE_TENANT= diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7b51ca0..5e39b94 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.17.0" + ".": "0.18.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8deea41..ed506c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +## [0.18.0](https://github.com/eSlider/go-onlyoffice/compare/v0.17.0...v0.18.0) (2026-09-04) + + +### Features + +* **crm:** add UpdateContactName and CloseCRMTask helpers ([4d8af7a](https://github.com/eSlider/go-onlyoffice/commit/4d8af7a2fef1fcfd96ad913cefc6497e3389963b)) + + +### Bug Fixes + +* **crm:** deterministic sortBy=id in contact paged lists ([4d91726](https://github.com/eSlider/go-onlyoffice/commit/4d917261792203732ac739644c9c82124b2166eb)) + + +### Documentation + +* **funding:** eSlider support links (reverse-import GitHub e9c969a) ([d650a16](https://github.com/eSlider/go-onlyoffice/commit/d650a16a36037949505eb017c92bd3283e6e2f0f)) + ## [0.17.0](https://github.com/eSlider/go-onlyoffice/compare/v0.16.0...v0.17.0) (2026-08-31) diff --git a/README.md b/README.md index 50870c4..1458a5f 100644 --- a/README.md +++ b/README.md @@ -678,6 +678,25 @@ oo dav download 22881 --to ./copy.pdf # default path: ./ oo dav fileops # active move/copy operations (status polling) ``` +### Search (`oo search`) + +Full-text search over the Documents index. The REST endpoint +`/api/2.0/files/@search/{query}` only searches file names in the database, so +`oo search` talks to the OnlyOffice **Elasticsearch** directly (index +`files_file`). Name search is default; `--content` also matches extracted +document text (`document.attachment.content`, Office formats only). +See [`docs/elasticsearch.md`](docs/elasticsearch.md) for the tunnel setup. + +```bash +oo search "Rechnung" # names only +oo search "Mahngebühr" --content # names + document text +oo search "Rechnung" --folder 649 --limit 50 +oo search "Rechnung" --json # shorthand for -o json +``` + +Requires `ONLYOFFICE_ES_URL` (plus optional `ONLYOFFICE_ES_INDEX`, +`ONLYOFFICE_TENANT`). + ### Bulk tools (`cmd/`) Small single-purpose binaries for bulk Documents work. All of them pace @@ -714,6 +733,7 @@ kontolink IN.xlsx oo-index.tsv OUT.xlsx [FILE_ID] [AMOUNTS_TSV] | `docs` | `tools`, `convert`, `optimize`, `ocr`, `hocr`, `as-md`, `put-md`, `put-txt`, `put-xlsx` | | `catalog` | `match`, `merge`, `apply`, `scan-contacts`, `scan-projects`, `scan-thunderbird` | | `dav` | `ls`, `move`, `copy`, `mkdir`, `rename-file`, `rename-folder`, `download`, `fileops` | +| `search` | `QUERY` (`--content`, `--folder ID`, `--limit N`, `--json`) | The CLI reads only `.env` from the current working directory (godotenv is a CLI-only concern — the library itself never loads dotfiles). diff --git a/cmd/oo/dav.go b/cmd/oo/dav.go index 4683444..56272f4 100644 --- a/cmd/oo/dav.go +++ b/cmd/oo/dav.go @@ -25,6 +25,7 @@ func davCmd() *cobra.Command { cmd.AddCommand(davMoveCmd()) cmd.AddCommand(davCopyCmd()) cmd.AddCommand(davMkdirCmd()) + cmd.AddCommand(davRemoveCmd()) cmd.AddCommand(davRenameFileCmd()) cmd.AddCommand(davRenameFolderCmd()) cmd.AddCommand(davDownloadCmd()) @@ -184,6 +185,32 @@ func davMkdirCmd() *cobra.Command { } } +func davRemoveCmd() *cobra.Command { + var folderIDs []string + cmd := &cobra.Command{ + Use: "rm [FILE_ID...]", + Aliases: []string{"delete"}, + Short: "Permanently delete file(s) and/or folder(s) from Documents", + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && len(folderIDs) == 0 { + return fmt.Errorf("dav rm: give at least one FILE_ID or --folders") + } + c, err := newOO(cmd) + if err != nil { + return err + } + if err := c.DeleteDavItems(cmd.Context(), folderIDs, args); err != nil { + return err + } + printObject(map[string]any{"deleted_files": args, "deleted_folders": folderIDs}) + return nil + }, + } + cmd.Flags().StringSliceVar(&folderIDs, "folders", nil, "folder ids to delete") + return cmd +} + func davRenameFileCmd() *cobra.Command { return &cobra.Command{ Use: "rename-file FILE_ID NEW_TITLE", diff --git a/cmd/oo/main.go b/cmd/oo/main.go index c91baaa..82ab8e8 100644 --- a/cmd/oo/main.go +++ b/cmd/oo/main.go @@ -18,6 +18,7 @@ // oo docs tools | convert | optimize | ocr | hocr | as-md | put-md | put-txt | put-xlsx // oo catalog match | merge | apply | scan-contacts | scan-projects | scan-thunderbird // oo dav ls | move | copy | mkdir | rename-file | rename-folder | download | fileops +// oo search QUERY [--content] [--folder ID] [--limit N] [--json] // // CRM association rules: docs/crm-associations.md // diff --git a/cmd/oo/search.go b/cmd/oo/search.go new file mode 100644 index 0000000..2ceab0e --- /dev/null +++ b/cmd/oo/search.go @@ -0,0 +1,72 @@ +package main + +import ( + onlyoffice "github.com/eslider/go-onlyoffice" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(searchCmd()) +} + +// searchCmd queries the OnlyOffice Elasticsearch index directly. The REST +// /api/2.0/files/@search endpoint only searches file names in the database; +// content search needs ES (see docs/elasticsearch.md). +func searchCmd() *cobra.Command { + var ( + content bool + folder string + limit int + asJSON bool + ) + cmd := &cobra.Command{ + Use: "search QUERY", + Short: "Full-text search over documents by name, optionally by content (Elasticsearch)", + Long: "Search the OnlyOffice Documents index.\n\n" + + "By default only file names are matched. With --content the query also\n" + + "matches extracted document text (document.attachment.content); this covers\n" + + "Office formats (docx/xlsx/pptx) and is slower.\n\n" + + "Requires ONLYOFFICE_ES_URL (and optionally ONLYOFFICE_ES_INDEX,\n" + + "ONLYOFFICE_TENANT). See docs/elasticsearch.md for the tunnel setup.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if asJSON { + outputFormat = "json" + } + es, err := onlyoffice.NewESSearcher(onlyoffice.ESConfigFromEnv()) + if err != nil { + return err + } + hits, err := es.Search(cmd.Context(), onlyoffice.SearchQuery{ + Text: args[0], + InContent: content, + FolderID: folder, + Limit: limit, + }) + if err != nil { + return err + } + rows := make([]map[string]any, 0, len(hits)) + for _, h := range hits { + rows = append(rows, map[string]any{ + "id": h.ID, + "title": h.Title, + "folder": h.ParentID, + "score": h.Score, + "highlight": h.Highlight, + }) + } + if outputFormat == "json" { + printJSON(rows) + return nil + } + printTable([]string{"id", "title", "folder", "score", "highlight"}, rows) + return nil + }, + } + cmd.Flags().BoolVar(&content, "content", false, "also match extracted document content") + cmd.Flags().StringVar(&folder, "folder", "", "limit to a Documents folder id") + cmd.Flags().IntVar(&limit, "limit", 20, "maximum number of results") + cmd.Flags().BoolVar(&asJSON, "json", false, "shorthand for --output json") + return cmd +} diff --git a/cmd/oo/search_test.go b/cmd/oo/search_test.go new file mode 100644 index 0000000..ea1716c --- /dev/null +++ b/cmd/oo/search_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "strings" + "testing" +) + +func TestSearchCommandRegisteredWithFlags(t *testing.T) { + cmd, _, err := rootCmd.Find([]string{"search"}) + if err != nil { + t.Fatal(err) + } + if cmd.Name() != "search" { + t.Fatalf("search resolved to %q", cmd.Name()) + } + for _, name := range []string{"content", "folder", "limit", "json"} { + if cmd.Flags().Lookup(name) == nil { + t.Errorf("search: missing --%s flag", name) + } + } + if got := cmd.Flags().Lookup("limit").DefValue; got != "20" { + t.Errorf("--limit default = %q, want 20", got) + } +} + +func TestSearchWithoutESURLIsClearError(t *testing.T) { + clearEnv(t, "ONLYOFFICE_ES_URL", "ONLYOFFICE_ES_INDEX", "ONLYOFFICE_TENANT") + errBuf := &bytes.Buffer{} + rootCmd.SetErr(errBuf) + rootCmd.SetOut(&bytes.Buffer{}) + rootCmd.SetArgs([]string{"search", "Rechnung"}) + t.Cleanup(func() { + rootCmd.SetArgs(nil) + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + }) + + err := rootCmd.Execute() + if err == nil { + t.Fatal("expected error without ONLYOFFICE_ES_URL") + } + if !strings.Contains(err.Error(), "ONLYOFFICE_ES_URL") { + t.Fatalf("error %q missing ONLYOFFICE_ES_URL", err.Error()) + } +} diff --git a/cmd/pdfamount/main.go b/cmd/pdfamount/main.go index d6b991e..6690caa 100644 --- a/cmd/pdfamount/main.go +++ b/cmd/pdfamount/main.go @@ -18,7 +18,52 @@ import ( onlyoffice "github.com/eslider/go-onlyoffice" ) -var amountRe = regexp.MustCompile(`(?i)(zu zahlender betrag|rechnungsbetrag)\s*[:\s]*([0-9][0-9.]*,[0-9]{2})`) +// amountPat is the amount capture shared by every amount regex. +const amountPat = `([0-9]+(?:[.,][0-9]+)*)` + +// amountRE builds "