Skip to content

Files and URIs

Bellui names files and other resources with a bellui.URI rather than a path. A URI starts with a scheme, such as file:, and the scheme decides which code handles it. That handler is called a repository. This is how a file chosen on an Android phone (a content: URI) and a file on a desktop disk (a file: URI) can go through the same functions.

The functions in the storage package look up the repository for a URI and call it. Use them rather than the os package when your code has to work on every platform.

Making URIs

dir := storage.NewFileURI(filepath.Join(os.TempDir(), "bellui-demo"))
file, err := storage.Child(dir, "hello.txt")
if err != nil {
	return err
}
log.WriteString(file.String() + "\n")    // file:///…/bellui-demo/hello.txt
log.WriteString(file.Name() + "\n")      // hello.txt
log.WriteString(file.Extension() + "\n") // .txt
log.WriteString(file.MimeType() + "\n")  // text/plain

A URI also tells you its Name(), Extension() (with the dot), MimeType(), Scheme() and Path(). Compare two URIs with storage.EqualURI.

Writing and reading

storage.Writer opens a resource for writing. For a file it creates the file, or empties it if it already exists. Always check the error from Close: it is part of saving.

wr, err := storage.Writer(file)
if err != nil {
	return err
}
if _, err := wr.Write([]byte("Hello from Bellui\n")); err != nil {
	wr.Close()
	return err
}
if err := wr.Close(); err != nil {
	return err
}

storage.Reader opens it for reading.

r, err := storage.Reader(file)
if err != nil {
	return err
}
defer r.Close()
data, err := io.ReadAll(r)
if err != nil {
	return err
}
log.WriteString("read: " + string(data))

storage.Appender opens a resource to add to its end. CanRead and CanWrite tell you in advance whether reading or writing is possible.

If you only need the bytes as a bellui.Resource, for example to show an image, storage.LoadResourceFromURI reads the whole resource at once.

Folders and listing

A resource that can contain others, such as a folder, is called listable. storage.CreateListable creates one. For files this is “make a folder”; it fails if the parent does not exist and does not create parents for you.

if ok, err := storage.Exists(dir); err != nil {
	return err
} else if !ok {
	if err := storage.CreateListable(dir); err != nil {
		return err
	}
}

storage.List returns the URIs inside a listable resource. CanList tells you whether a URI is listable, and storage.ListerForURI converts a URI into a bellui.ListableURI, the type the folder dialog returns.

children, err := storage.List(dir)
if err != nil {
	return err
}
for _, child := range children {
	log.WriteString("child: " + child.Name() + "\n")
}

storage.IsHidden applies the platform’s rule for hidden files: a leading dot on Unix-like systems, the hidden attribute on Windows. It always reports false on mobile and the web.

Copying, moving and deleting

backup, _ := storage.Child(dir, "hello.bak")
if err := storage.Copy(file, backup); err != nil {
	return err
}
if err := storage.Delete(backup); err != nil {
	return err
}

storage.Copy, storage.Move, storage.Delete and storage.DeleteAll do what their names say. DeleteAll removes a folder and everything in it.

Which schemes work where

Not every repository supports every operation. When an operation is not available for a URI’s scheme, the function returns repository.ErrOperationNotSupported. The schemes Bellui registers are:

SchemeWhereNotes
fileDesktop, Android, iOSLocal files. Full read, write and list support on the desktop.
contentAndroidFiles handed over by the Android document picker.
http, httpsEverywhereReading from the web.
idbfileWebThe browser’s IndexedDB, used for app storage on the web.

To add your own scheme, see Custom repositories.

The complete example

// Reading, writing and listing files through URIs.
package main

import (
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/storage"
	"github.com/belluikit/bellui/widget"
)

func main() {
	a := app.New()
	w := a.NewWindow("Files and URIs")
	out := widget.NewLabel("")

	run := widget.NewButton("Run", func() {
		var log strings.Builder
		if err := demo(&log); err != nil {
			log.WriteString("error: " + err.Error() + "\n")
		}
		out.SetText(log.String())
	})

	w.SetContent(container.NewVBox(run, out))
	bellui.ShowAndRun(w)
}

func demo(log *strings.Builder) error {
	dir := storage.NewFileURI(filepath.Join(os.TempDir(), "bellui-demo"))
	file, err := storage.Child(dir, "hello.txt")
	if err != nil {
		return err
	}
	log.WriteString(file.String() + "\n")    // file:///…/bellui-demo/hello.txt
	log.WriteString(file.Name() + "\n")      // hello.txt
	log.WriteString(file.Extension() + "\n") // .txt
	log.WriteString(file.MimeType() + "\n")  // text/plain

	if ok, err := storage.Exists(dir); err != nil {
		return err
	} else if !ok {
		if err := storage.CreateListable(dir); err != nil {
			return err
		}
	}

	wr, err := storage.Writer(file)
	if err != nil {
		return err
	}
	if _, err := wr.Write([]byte("Hello from Bellui\n")); err != nil {
		wr.Close()
		return err
	}
	if err := wr.Close(); err != nil {
		return err
	}

	r, err := storage.Reader(file)
	if err != nil {
		return err
	}
	defer r.Close()
	data, err := io.ReadAll(r)
	if err != nil {
		return err
	}
	log.WriteString("read: " + string(data))

	children, err := storage.List(dir)
	if err != nil {
		return err
	}
	for _, child := range children {
		log.WriteString("child: " + child.Name() + "\n")
	}

	backup, _ := storage.Child(dir, "hello.bak")
	if err := storage.Copy(file, backup); err != nil {
		return err
	}
	if err := storage.Delete(backup); err != nil {
		return err
	}
	return nil
}