A Go client library for the iRODS HTTP API.
Designed after the Python (irods_client_http_python) and Java (irods_client_http_java) reference implementations with Go-idiomatic conventions:
context.Contextsupport on every call- Multiple return values (
value, error) instead of exceptions - Streaming I/O (
io.Reader/io.WriterAt) for large file transfers - Goroutine-safe client with connection pooling
- No external dependencies — standard library only
- Go 1.21 or later
go get github.com/irods/irods_client_http_gopackage main
import (
"context"
"fmt"
"log"
"github.com/irods/irods_client_http_go/irods"
)
func main() {
ctx := context.Background()
client := irods.New("https://irods-http.example.org:9000")
if err := client.Authenticate(ctx, "alice", "password"); err != nil {
log.Fatal(err)
}
resp, err := client.DataObject().Stat(ctx, "/zone/home/alice/data.txt", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Body)
}client := irods.New("https://irods-http.example.org:9000")
// Username / password (HTTP Basic -> Bearer token)
err := client.Authenticate(ctx, "alice", "password")
// Pre-existing token (e.g. OpenID Connect)
client := irods.NewWithToken("https://irods-http.example.org:9000", "existing-token")
// Replace the token at any time (goroutine-safe)
client.SetToken("new-token")client := irods.New(
"https://irods-http.example.org:9000",
irods.WithDialTimeout(10 * time.Second),
irods.WithResponseHeaderTimeout(30 * time.Second),
irods.WithMaxIdleConnsPerHost(20), // increase for parallel workloads
irods.WithDisableCompression(true), // for pre-compressed data
)Per-operation timeouts are set via context.WithTimeout at the call site:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
resp, err := client.DataObject().Read(ctx, "/zone/home/alice/large.bin", nil)do := client.DataObject()
// Stat
resp, err := do.Stat(ctx, "/zone/home/alice/data.txt", nil)
// Read small object into memory
resp, err := do.Read(ctx, "/zone/home/alice/data.txt", nil)
fmt.Println(resp.Body)
// Stream a large object (caller must close Body)
stream, err := do.ReadStream(ctx, "/zone/home/alice/large.bin", nil)
if err != nil { log.Fatal(err) }
defer stream.Body.Close()
io.Copy(os.Stdout, stream.Body)
// Write small object
err = func() error {
_, err := do.Write(ctx, "/zone/home/alice/hello.txt", []byte("hello"), nil)
return err
}()
// Stream-write a large object
f, _ := os.Open("/local/large.bin")
defer f.Close()
_, err = do.WriteStream(ctx, "/zone/home/alice/large.bin", f, nil)
// Upload local file using parallel streams
_, err = do.UploadFile(ctx, "/zone/home/alice/large.bin", "/local/large.bin", 4, nil)
// Download to local file using parallel streams
_, err = do.DownloadFile(ctx, "/zone/home/alice/large.bin", "/local/copy.bin", 4, nil)
// Touch (create or update mtime)
_, err = do.Touch(ctx, "/zone/home/alice/new.txt", nil)
// Remove
_, err = do.Remove(ctx, "/zone/home/alice/old.txt", 0, nil)
// Rename / Move
_, err = do.Rename(ctx, "/zone/home/alice/old.txt", "/zone/home/alice/new.txt")
// Copy
_, err = do.Copy(ctx, "/zone/home/alice/src.txt", "/zone/home/alice/dst.txt", nil)
// Replicate to another resource
_, err = do.Replicate(ctx, "/zone/home/alice/data.txt", "", "demoResc2", nil)
// Checksum
_, err = do.CalculateChecksum(ctx, "/zone/home/alice/data.txt", nil)
_, err = do.VerifyChecksum(ctx, "/zone/home/alice/data.txt", nil)
// Metadata (AVU)
_, err = do.ModifyMetadata(ctx, "/zone/home/alice/data.txt", []irods.ModifyMetadataOperations{
{Operation: irods.MetadataOperationAdd, Attribute: "project", Value: "wilma"},
{Operation: irods.MetadataOperationAdd, Attribute: "year", Value: "2026", Units: "CE"},
}, nil)
// Permissions
_, err = do.SetPermission(ctx, "/zone/home/alice/data.txt", "bob", irods.PermissionRead, nil)
_, err = do.ModifyPermissions(ctx, "/zone/home/alice/data.txt", []irods.ModifyPermissionsOperations{
{EntityName: "bob", ACL: irods.PermissionRead},
{EntityName: "carol", ACL: irods.PermissionOwn},
}, nil)// Manual parallel write — use UploadFile for the common case.
initResp, err := do.ParallelWriteInit(ctx, "/zone/home/alice/large.bin", 4, nil)
// parse handle from initResp.Body ...
_, err = do.ParallelWriteShutdown(ctx, handle)
// High-level helper: manages init / fan-out / shutdown automatically
f, _ := os.Open("/local/large.bin")
defer f.Close()
_, err = do.ParallelWrite(ctx, "/zone/home/alice/large.bin", f, 4, nil)// ParallelRead writes each chunk directly to the correct file offset via io.WriterAt —
// no reordering buffer needed. *os.File implements io.WriterAt.
out, _ := os.Create("/local/copy.bin")
defer out.Close()
_, err = do.ParallelRead(ctx, "/zone/home/alice/large.bin", out, 4, nil)col := client.Collections()
// Create (with intermediate paths)
_, err := col.Create(ctx, "/zone/home/alice/a/b/c", irods.IntPtr(1))
// List
resp, err := col.List(ctx, "/zone/home/alice", nil)
// Stat
resp, err := col.Stat(ctx, "/zone/home/alice", nil)
// Remove recursively
_, err = col.Remove(ctx, "/zone/home/alice/tmp", &irods.CollectionsRemoveParams{
Recurse: irods.IntPtr(1),
})
// Rename / Move
_, err = col.Rename(ctx, "/zone/home/alice/old", "/zone/home/alice/new")
// Permission inheritance
_, err = col.SetInheritance(ctx, "/zone/home/alice/shared", 1, nil)
// Metadata
_, err = col.ModifyMetadata(ctx, "/zone/home/alice", []irods.ModifyMetadataOperations{
{Operation: irods.MetadataOperationAdd, Attribute: "tag", Value: "important"},
}, nil)ug := client.UserGroupOperations()
// Create user
_, err := ug.CreateUser(ctx, "bob", "zone", nil)
// Set password
_, err = ug.SetPassword(ctx, "bob", "zone", "s3cr3t")
// Create group and add member
_, err = ug.CreateGroup(ctx, "scientists")
_, err = ug.AddToGroup(ctx, "bob", "zone", "scientists")
// Check membership
resp, err := ug.IsMemberOfGroup(ctx, "scientists", "bob", "zone")
// List
resp, err = ug.Users(ctx)
resp, err = ug.Groups(ctx)resc := client.ResourceOperations()
// Create a unixfilesystem resource
_, err := resc.Create(ctx, "myResc", "unixfilesystem", &irods.ResourceCreateParams{
Host: irods.StringPtr("icat.example.org"),
VaultPath: irods.StringPtr("/srv/irods/myResc"),
})
// Build a hierarchy
_, err = resc.AddChild(ctx, "parentResc", "myResc", nil)
// Modify a property
_, err = resc.Modify(ctx, "myResc", irods.ResourcePropertyStatus, "up")
// Rebalance
_, err = resc.Rebalance(ctx, "parentResc")
// Stat
resp, err := resc.Stat(ctx, "myResc")zones := client.ZoneOperations()
// Add remote zone
_, err := zones.Add(ctx, "remoteZone", &irods.ZoneAddParams{
ConnectionInfo: irods.StringPtr("remote.example.org:1247"),
})
// Report and stat
resp, err := zones.Report(ctx)
resp, err = zones.Stat(ctx, "remoteZone")tk := client.TicketOperations()
// Create a read ticket limited to 10 uses
resp, err := tk.Create(ctx, "/zone/home/alice/data.txt", &irods.TicketCreateParams{
Type: irods.StringPtr("read"),
UseCount: irods.IntPtr(10),
})
// Remove
_, err = tk.Remove(ctx, "ticketName")q := client.QueryOperations()
// GenQuery
resp, err := q.ExecuteGenQuery(ctx,
"SELECT COLL_NAME, DATA_NAME WHERE COLL_NAME = '/zone/home/alice'",
&irods.QueryExecuteGenQueryParams{Count: irods.IntPtr(100)},
)
// Specific query
resp, err = q.ExecuteSpecificQuery(ctx, "ShowCollAcls", &irods.QueryExecuteSpecificQueryParams{
Args: irods.StringPtr("/zone/home/alice"),
})rules := client.RuleOperations()
// List available rule engine plugins
resp, err := rules.ListRuleEngines(ctx)
// Execute a rule
_, err = rules.Execute(ctx, `myRule { writeLine("serverLog", "hello"); }`, nil)
// Remove a delay rule
_, err = rules.RemoveDelayRule(ctx, 42)// No authentication required
resp, err := client.Information().Get(ctx)
fmt.Println(resp.Body)IRODSFileHandle provides an open/read/write/close lifecycle for use cases such as FUSE.
Each ReadAt and WriteAt maps to one HTTP request at the specified offset.
Contiguous small writes are coalesced in an internal buffer to reduce HTTP overhead.
do := client.DataObject()
// Open for read/write with a 4 MB write buffer
h, err := do.OpenFile(ctx, "/zone/home/alice/data.bin", irods.OpenFlagReadWrite, 4*1024*1024)
if err != nil { log.Fatal(err) }
defer h.Close(ctx)
// Random read
buf := make([]byte, 512)
n, err := h.ReadAt(ctx, buf, 1024)
// Random write (buffered)
_, err = h.WriteAt(ctx, []byte("patch"), 2048)
// Explicit flush (e.g. on FUSE fsync)
err = h.Flush(ctx)Three distinct error types allow callers to handle failures at the right granularity:
resp, err := client.DataObject().Stat(ctx, "/zone/home/alice/data.txt", nil)
if err != nil {
var paramErr *irods.ErrInvalidParam
var httpErr *irods.ErrHTTP
var irodsErr *irods.ErrIRODS
switch {
case errors.As(err, ¶mErr):
// bad argument before the request was sent
fmt.Println("invalid parameter:", paramErr.Param, paramErr.Message)
case errors.As(err, &httpErr):
// 4xx / 5xx HTTP response
fmt.Println("HTTP error:", httpErr.StatusCode)
case errors.As(err, &irodsErr):
// HTTP 200 but iRODS reported a non-zero status code
fmt.Println("iRODS error:", irodsErr.StatusCode, irodsErr.StatusMessage)
default:
fmt.Println("network or context error:", err)
}
}Optional parameter fields use pointer types. Use the provided helpers to avoid temporary variables:
irods.StringPtr("demoResc") // *string
irods.IntPtr(1) // *int
irods.Int64Ptr(4096) // *int64Copyright (c) 2010-2026, The Arizona Board of Regents on behalf of The University of Arizona. See LICENSE for details.