Getting started
This binding is unreleased and experimental. Build from source before following this guide. See Overview for the build steps.
Your first program
The example below uses the memory service, which requires no credentials and
runs entirely in-process. Open the project in utop or add it to a dune
executable that depends on the opendal library.
open Opendal
(* Return the value, or print the error and exit. Every OpenDAL call returns a
[(_, string) result], so this keeps the example flat. *)
let or_fail = function
| Ok v -> v
| Error err ->
prerr_endline err;
exit 1
let () =
(* Create an operator for the in-memory service — no credentials needed. *)
let op = or_fail (Operator.new_operator "memory" []) in
(* Write a file, then read it back (read returns a char array). *)
or_fail (Operator.write op "hello.txt" (Bytes.of_string "Hello, World!"));
let content = or_fail (Operator.read op "hello.txt") in
let text = content |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
Printf.printf "read: %s\n" text;
(* Inspect metadata, then delete. *)
let meta = or_fail (Operator.stat op "hello.txt") in
Printf.printf "size = %Ld bytes\n" (Operator.Metadata.content_length meta);
or_fail (Operator.delete op "hello.txt")
Operator.read returns (char array, string) result. Convert to Bytes with
Array.to_seq |> Bytes.of_seq and then to string with Bytes.to_string.
Point it at a real backend
Only the scheme and config map change; every operation stays the same:
open Opendal
let () =
let config =
[ ("bucket", "my-bucket")
; ("region", "us-east-1")
; ("access_key_id", "...")
; ("secret_access_key", "...")
]
in
match Operator.new_operator "s3" config with
| Error err -> Printf.eprintf "Failed: %s\n" err
| Ok op ->
ignore (Operator.write op "hello.txt" (Bytes.of_string "Hello from S3!"))
See Services for every backend and its configuration keys.
Error handling
Every fallible function returns ('a, string) result. The idiomatic pattern is
match:
match Operator.read op "maybe-missing.txt" with
| Ok content ->
(* use content *)
let text = content |> Array.to_seq |> Bytes.of_seq |> Bytes.to_string in
print_endline text
| Error msg ->
(* msg is a string description of the error *)
Printf.eprintf "Error: %s\n" msg
You can also use Result.get_ok to raise an exception on error (useful in
tests and scripts):
let content = Operator.read op "file.txt" |> Result.get_ok in
Streaming reads and writes
For large files, avoid loading everything into memory:
(* Streaming read — read 1 KiB starting at byte offset 0 *)
match Operator.reader op "big.bin" with
| Error err -> Printf.eprintf "Reader failed: %s\n" err
| Ok r ->
let buf = Bytes.create 1024 in
(match Operator.Reader.pread r buf 0L with
| Ok n -> Printf.printf "Read %d bytes\n" n
| Error err -> Printf.eprintf "pread failed: %s\n" err)
(* Streaming write — write in chunks, then close to commit *)
match Operator.writer op "big.bin" with
| Error err -> Printf.eprintf "Writer failed: %s\n" err
| Ok w ->
ignore (Operator.Writer.write w (Bytes.of_string "first chunk\n"));
ignore (Operator.Writer.write w (Bytes.of_string "second chunk\n"));
(match Operator.Writer.close w with
| Ok _meta -> print_endline "Upload complete"
| Error err -> Printf.eprintf "Close failed: %s\n" err)
Writer.close must be called to commit the upload; it returns metadata for the
written object.
Listing a directory
list loads all entries into an array; lister streams them one at a time:
(* Batch listing *)
match Operator.list op "data/" with
| Error err -> Printf.eprintf "List failed: %s\n" err
| Ok entries ->
Array.iter
(fun entry -> Printf.printf "%s\n" (Operator.Entry.name entry))
entries
(* Streaming listing — memory-efficient for large directories *)
match Operator.lister op "data/" with
| Error err -> Printf.eprintf "Lister failed: %s\n" err
| Ok lst ->
let rec loop () =
match Operator.Lister.next lst with
| Ok (Some entry) ->
Printf.printf "%s\n" (Operator.Entry.name entry);
loop ()
| Ok None -> () (* end of listing *)
| Error err -> Printf.eprintf "Next failed: %s\n" err
in
loop ()
Checking capabilities
Not every service supports every operation. Query capabilities before calling
optional operations like copy or rename:
let info = Operator.info op in
let cap = Operator.OperatorInfo.capability info in
if Operator.Capability.copy cap then
ignore (Operator.copy op "source.txt" "backup.txt")
else
print_endline "copy not supported by this backend"