harbouR 0.1.0
First release.
- Final minor refinements: constrain bundled asset paths to their extraction directory and reject colliding CSV filenames before writing files.
- Exclude local administration and repository citation metadata from source builds, and use the repository citation URL until the archival record is publicly available.
harbouR is an unofficial R client for the SeaTable REST API. It connects to a SeaTable server, reads and writes rows as tidy tibbles, manages tables, columns and views, uploads and attaches files, reads and writes local .dtable exports with no server at all, and ships a Shiny explorer for doing all of that without writing code.
The notes below record what changed during development, against an 0.1.0 that was tagged but never published.
The maintainer address is now
raban.heller@uni-ulm.de, andURL/BugReportspoint at https://github.com/CTTIR/harbouR, which is where the package actually lives. Paul Elsinghorst, Wiebke Derz, Matthias Ring, Gerhard Achatz and Vinzent Forstmeier are credited as contributors.hb_example_metadata()now reads the demo base frominst/extdata/example_metadata.jsonrather than duplicating it in R source, so there is a single definition to maintain.HTTP 404 responses now name the failing endpoint instead of erroring while trying to format the message.
httptest2has been dropped fromSuggests: it was never used, and its absence turnedR CMD checkred on any machine without it. HTTP is replaced at the package’s own request seam instead.Errors now carry condition classes under a shared
harbour_error, so they can be caught by kind rather than by message text. See?"harbouR-conditions". HTTP errors carrystatus,urlandbody.A
403response is no longer retried as though the base token had expired. Only401triggers a refresh.hb_ping()andhb_server_info()no longer send anAuthorizationheader. Both endpoints are unauthenticated, and demanding an API token made them fail for username/password clients - exactly the case where checking connectivity matters most.hb_ping()now returns the client invisibly rather thanTRUE, so it composes:client |> hb_ping() |> hb_read_table("Samples"). Newhb_check_credentials()covers the other half of the old behaviour - proving the credentials are accepted, not just that the server is up.Reading a table whose schema contains a column literally named
_idnow errors with aharbour_error_column_collisioncondition instead of silently overwriting it with the SeaTable row identifier.hb_run_explorer()restores the previous value of theharbouR_preset_clientShiny option when it exits. A client carrying a plaintext token used to stay reachable in process-global state for the rest of the session.Every base-scoped request now addresses a real resource. The base UUID was captured from the token exchange and then never interpolated into a URL, so all 24 base-scoped paths pointed at a collection with no base identifier.
hb_metadata()additionally used a service prefix,/dtables/api/v1/, that does not exist - and because every other function lazily depends on it, its failure cascaded to all of them.harbouR now targets the SeaTable API gateway.
/dtable-server/and/dtable-db/were deprecated in SeaTable 5.2 and removed in 5.3; harbouR addressed them exclusively. Base operations move to/api-gateway/api/v2/dtables/{base_uuid}/, which also collapses the three-host service switch to one. Base tokens are sent asBearer, which is what the gateway expects; the web API keepsToken. This requires SeaTable 5.3 (June 2025) or newer.
Legal position
NOTICE.md, the README, the vignette and?harbouRnow cite the basis on which harbouR uses SeaTable’s interface, rather than implying it rests on a licence grant. Endpoint paths, HTTP verbs and JSON field names are the ideas and principles underlying an interface, excluded from copyright protection by Article 1(2) of Directive 2009/24/EC and by the CJEU in SAS Institute v World Programming (C-406/10) - which was referred, on these facts, the question whether a program may be written to read and write another’s data file format.They also cite the clause in SeaTable’s own dtable-server EULA that defines applications communicating over the API as “third-party software” and states that the EULA’s provisions “do not apply to any such third-party software”.
Two factual corrections: the per-component licences previously attributed to SeaTable Server could not be verified from the repositories, and the API specification is published publicly but is not openly licensed. Both statements are gone. The one verbatim quotation in the package is now attributed to its source.
The wording is deliberately narrow. harbouR relies on the interface exclusion rather than asserting that it settles the question, because it does not settle everything: the CJEU reserved the possibility that a data file format could attract ordinary copyright as a work (C-406/10, para. 45), and § 69g(1) UrhG leaves unfair competition, trade secrets, trademarks and contract untouched. Both limits are stated in
NOTICE.md, along with the fact that none of this is legal advice.
Scope
harbouR is stated, throughout, as being for two things: SeaTable
.dtabledata files, read and written directly with no server; and a SeaTable Server you run yourself, over its REST API. The Title and Description say so, and there is a “What harbouR is for” section in the README and in?harbouR.The explorer’s server field no longer pre-fills a vendor-hosted address; it starts empty with a neutral placeholder. The documented examples for
hb_client()and for the live tests use a self-hosted URL.Nothing here restricts where you point the package.
hb_client()takes a server URL as an argument and makes no judgement about the host; the scope statement describes what harbouR is built and documented for, not what it will refuse to do.
Acknowledgement
-
New Acknowledgements section in the README, the introductory vignette and
?harbouR, naming what SeaTable GmbH and Seafile Ltd. built that made an independent client possible: a publicly published OpenAPI specification kept per server version, a reference readable without an account, manuals in repositories that accept pull requests, Apache-2.0 client libraries, a Developer Edition you can host yourself - and a changelog that records removals as plainly as additions. That last one is not abstract: it is how harbouR learned it had been aimed at endpoints removed in SeaTable 5.3.The section is careful about what it does and does not claim: SeaTable Server is not open source, and it says so. A later adversarial review found two assertions in the first version of this text that were not in fact supported by their sources - the per-component licences of SeaTable Server, and the claim that the API specification is “openly licensed” - and both have been corrected. harbouR needs no licence to the specification: endpoint paths, verbs and field names are excluded from copyright protection as the ideas underlying an interface.
Authorship and citation
Paul Elsinghorst, Wiebke Derz, Matthias Ring, Gerhard Achatz and Vinzent Forstmeier are credited as authors (
aut), not contributors.New
inst/CITATION, socitation("harbouR")produces a proper reference and BibTeX entry naming every author. It reads the package’s own metadata, so the authors, version and year cannot drift out of step withDESCRIPTION. README and the introductory vignette both carry a Citation section.
Fixes from the post-rewrite audit
Security: the explorer’s Schema tab rendered a column’s type string as raw HTML. A
.dtablecarrying a crafted type could run script in a session that may hold the user’s API token. The type is now escaped.Data loss:
hb_write_dtable()dropped the bundledasset/tree unless the base had been read withassets = "extract", producing an archive whose attachment cells pointed at files it did not contain. The explorer now extracts on open, and the writer warns rather than discarding silently.Exporting a live base to
.dtablewrote_idas an ordinary column, which then collided with the reserved name on read - the export could not be re-opened. System fields are dropped first.hb_update_rows()threw a bare “subscript out of bounds” for any column not in the table’s schema, which is whathb_read_table() |> mutate() |> hb_update_rows()produces. It now names the offending column. It also drops server-computed columns, whichhb_append_rows()already did.hb_duplicate_table()sent an undocumentednew_table_nameand omitted the requiredis_duplicate_records. It gainsduplicate_recordsand documents that SeaTable always names the copy<original> (copy).hb_create_table()documented column specs asname/typewhile the API wantscolumn_name/column_type, so following the package’s own example produced a body the server rejects. Both spellings now work.hb_client()andhb_read_dtable()validate their arguments before using them, so a wrong type gives a classed harbouR error rather than a raw R one.Writing
InforNaNto a.dtablewarns. JSON cannot hold them, so they becomenulland read back asNA; that is now visible.The endpoint contract test pins request-body field names as well as paths. Renaming a body field previously left the suite green.
The explorer
The Shiny app has been rebuilt, and it lives in
R/. It was underinst/, whereR CMD check,covrandlintrcould not see it - which is how it came to use an operator that does not exist on three of the four R versions the package claims to support. Its logic is now driven headlessly byshiny::testServer()in the test suite, andcodecov.ymlno longer hides it from the coverage number.hb_run_explorer()opens a local.dtable, connects to a server, or loads a bundled example base that needs no credentials and no network. Its first argument takes either aharbour_clientor aharbour_dtable.Everything is reachable without code: browse tables, read the schema, run SQL, and download the base as
.dtable, Excel or zipped CSV - or the open table as a single CSV.The palette is the package logo’s, which is viridis. Column types are grouped into families, each keeping one colour throughout, and every table in the sidebar carries a band showing its columns by family - so the shape of a sixty-column table is legible before you open it.
The API token field is never pre-filled from the environment. A value passed to a password input is serialised into the page’s HTML.
Local .dtable files
harbouR reads and writes SeaTable
.dtableexports.hb_read_dtable()opens one;hb_write_dtable()writes it back. Round-tripping is lossless, verified against a real 750 KB export: read then write then read yields an identical structure, including fields harbouR does not model.The same verbs work on a file as on a live base.
hb_list_tables(),hb_list_columns(),hb_list_views()andhb_read_table()are now S3 generics withharbour_clientandharbour_dtablemethods, so an analysis can be written once and run either way. Their first argument is namedx; the write verbs keepclient, because they need a server.hb_dtable()builds a base from R data frames, inferring column types, so a set of tables can be written out and imported into SeaTable.hb_validate_dtable()reports what would stop a base importing, and the writer refuses to produce a file that would fail.hb_write_xlsx()andhb_write_csv()export to spreadsheets, naming exactly which columns had to be flattened;hb_read_xlsx()andhb_read_csv()build a new base from them. The lossy direction is documented rather than implied.Select-option ids are translated to their display names, because a file stores the id where the API returns the name - without this a local read and a server read of the same column disagreed.
hb_asset_path()resolves a bundledfile://dtable-bundle/URL to a local file when the base was read withassets = "extract".
The SeaTable API
hb_query()types its result from the schema SeaTable returns alongside the rows, which harbouR previously discarded. It inferred types from the values instead, so an all-NULLcolumn came backlogicaland one stray string turned a numeric column intocharacter- the same query could return different types on different days.hb_query()andhb_read_table()now agree.hb_query()warns when the SQL has noLIMITclause, because SeaTable applies an implicitLIMIT 100, and gainsparametersfor?placeholders andconvert_keys.The username/password flow works.
hb_client()acceptedusernameandpasswordand documented a three-token model, but every base call then aborted with “not supported” and every web call with “API token required”: such a client could do nothing at all. harbouR now performs the account-token to base-token exchange. Because an account token is scoped to the user rather than to a base,hb_client()gainsworkspace_idandbase_name, and says so if they are missing.Percent-encoded path segments no longer reach the server double-encoded.
httr2::req_url_path()escapes what it is given, so an already-escaped%20became%2520; it also leaves/alone, so a view nameda/bwould have split into two path segments.Dates parse.
_ctimeand_mtimecome back as full ISO-8601 with a UTC offset -2025-11-28T14:00:24.395+00:00- and the parser’s format list covered none of the offset-bearing forms, so every creation and modification time read asNA. It now handles theTseparator, fractional seconds and offsets, and honours the offset instead of discarding it.List-columns are type-stable. A
multiple-selectcolumn yielded acharactervector for a populated cell and alistfor an empty one, sotidyr::unnest(),purrr::map_chr()andvctrsall failed on a table containing a single blank cell. Empty and populated cells now agree, for all 28 types, and a contract test asserts it type by type.Reading a table with no columns returns a 0-row tibble carrying
_id, as documented, rather than a 0x0 tibble.An unrecognised column type reads as text rather than erroring, so a future SeaTable release cannot break a read.
long-textcells that arrive as an object rather than a string yield their text instead of a mangled coercion.Reading is roughly 70x faster: the type lookup rebuilt the column-type tibble once per cell. Reading the whole of a real 10-table base went from 27s to 0.4s.
Breaking:
hb_read_table(limit =)is nowpage_size =, and there is a newn_max =. The old argument was documented as a page size but was also used as the stop condition, solimit = 5000returned 1000 rows and stopped - a wrong answer with no warning.page_sizeis clamped to SeaTable’s 1000-row maximum with a warning,n_maxbounds the total, and a runaway server now produces an error rather than an infinite loop.hb_append_rows(),hb_update_rows()andhb_delete_rows()split their work into requests of at most 1000 rows, which is SeaTable’s batch limit. Writing 2500 rows used to send one oversized request. All three gain achunk_sizeargument.Breaking: the three write verbs return a one-row summary -
table,n_rows,n_requests- instead of fabricated per-row results.hb_append_rows()previously fell back to the request payload, which has no server-assigned_id, while promising server-generated ids in its documentation;hb_update_rows()andhb_delete_rows()returnedrep(TRUE, n)without looking at the response at all.n_rowsfromhb_append_rows()is the count the server confirmed.hb_upload_file()returnedurl = NAon every call, so the object it produced could never be written into a file cell - which is whathb_attach_file(), the flagship helper, does with it. It now requestsret-json=1(without it the endpoint replies in plain text, so the response was unparseable), uploads to the image or file tree the server nominates rather than a hard-codedfiles, and builds the asset URL from the workspace id, base UUID and upload path.hb_delete_asset()targeted/api/v2.1/dtable/asset/with an API token and aurlbody. The endpoint is/api/v2.1/dtable/app-asset/, takes a base token, and identifies the asset by apathquery parameter.Writing a file, image or geolocation cell no longer corrupts it. The write path flattened every list-valued cell with
unlist(), which is right for a multiple-select column but turns a file cell’s{name, size, type, url}object intoc("report.pdf", "12345", "application/pdf", "https://..."). Serialisation is now driven by the column type. Flat list-columns are emitted as JSON arrays even when they hold a single value, which previously auto-unboxed to a scalar and changed the cell’s type.hb_update_column()sends theop_typefield SeaTable requires; without it the request was rejected. It gains anew_typeargument for type changes, and refuses to rename and retype in one call, because SeaTable performs one operation per request.hb_update_select_option()sentoption/new_optionscalars. The API identifies an option by id, so harbouR now reads the column’s options and resolves the name first.hb_delete_select_option()sends theoption_namesarray the API expects rather than a scalaroption.hb_list_columns()andhb_list_views()read thecolumns/andviews/endpoints rather than only the cached base metadata, and both gain arefreshargument.hb_list_columns()gains adatalist-column carrying each column’s type-specific configuration - the select options, the date format - which is what makes option ids resolvable.hb_get_view()coerces its scalars like every sibling function, and reportsis_defaultandhidden_columns.Table, view and row names are percent-encoded into the path, so a view called
Erwartungswerte Toxine 1ora/bno longer produces a broken or ambiguous URL.hb_duplicate_table()posted totables/duplicate/; the endpoint istables/duplicate-table/.hb_list_collaborators()called a web-service path that does not exist. It is a base-scoped endpoint and needs the base token.hb_column_types()is now genuinely the single source of truth for the coercion layer. It gainsis_listandread_onlylogical columns, which the coercion functions derive their type sets from instead of repeating hard-coded vectors, and it covers five types it previously omitted:percent,dollar,euro,durationanddigital-sign.Breaking:
hb_list_tables()andas_tibble()on aharbour_metadatano longer return ann_rowscolumn. The metadata endpoint carries no row payloads, so the value was structurally always0and told every user their tables were empty.New
format()methods forharbour_clientandharbour_metadata, so their representation can be captured as a character vector rather than only printed.Breaking: the 39 scaffolded endpoints that only ever raised “not yet implemented” have been removed -
hb_list_links(),hb_create_base(),hb_export_table(), thehb_admin_*()andhb_team_*()families, and the rest. They were half the exported surface and none of them worked. The remaining areas are listed under Roadmap in the README.