FolderPicker
FolderPicker draws a button that opens a directory dialog on desktop or in
Chromium's Wasm executor.
It returns nil before a selection. Selecting a directory reruns the page, and
that run receives the new folder. Cancelling keeps the previous selection and
does not rerun.
API
type FolderObject = tgframe.Folder
type FolderPickerConf struct {
tgframe.Base // ID
Write bool
DefaultDir string
Disabled bool
}
func FolderPicker(c *tgframe.Container, label string, conf ...*FolderPickerConf) FolderObject
conf is optional, with at most one configuration. Write requests write
access; desktop grants it unconditionally. DefaultDir sets the desktop
dialog's initial directory. Disabled disables the button.
The shared interface lives in tgframe:
type Folder interface {
fs.ReadDirFS // Open and ReadDir
fs.StatFS
Create(name string) (io.WriteCloser, error)
MkdirAll(name string) error
Writable() bool
Path() string
}
var ErrFolderReadOnly = tgutil.NewError("folder is read-only")
Names follow fs.ValidPath: slash-separated paths relative to the selected
root, with no .. components. Desktop uses os.OpenRoot, which also prevents
symlinks from escaping the root. Create creates or truncates a file and
requires an existing parent. MkdirAll creates missing parents. Read-only
implementations return tgframe.ErrFolderReadOnly for either write operation.
Close files and writers returned by Open and Create when finished.
Writable reports granted write access. Path returns an absolute OS path on
desktop; other executors return an empty string. The session owns the folder
and closes it when replaced or the session ends; use it within the current
page run rather than keeping it across selections or sessions.
In Wasm, the dialog uses showDirectoryPicker in a secure context (HTTPS or
localhost). Write: true requests readwrite; otherwise it requests read
and Writable() is false even if the browser already has broader permission.
Cancelling or denying permission keeps the previous selection without rerunning.
DefaultDir is ignored in Wasm, and Path() is always empty.
The browser directory handle is passed directly to the Go worker. Reads use
File snapshots in chunks and writes use writable streams, committed on Close.
No network upload or download is involved. Handles are kept only for the session;
reloads require a new selection. Firefox, Safari, and the server Web executor
show a disabled button with an explanation and return nil.
Why FolderPicker is disabled in Web
Web executor support is intentionally not scheduled. Go runs on the server, while the browser owns the directory handle; even when both run on the same machine, a handle is not a server path. Implementing the same Folder interface would require browser filesystem requests and replies, run cancellation, session-bound permissions and a new transfer protocol. There is currently no concrete app requiring that added complexity.
For server processing, use MultiFileUpload and DownloadFile, optionally packaging output files into an archive. For reading and writing a local tree in place, use the desktop or Wasm executor. Upload/download does not provide in-place folder access, but it covers the current import/process/export examples without granting a server ongoing access to local files.
The existing disabled-control explanation, "Folder selection is not available in this executor yet.", does not promise future Web support. See the Web FolderPicker decision for the protocol sketch, security and performance assessment, and criteria for reconsidering support. The disabled behavior and message are unchanged by this documentation-only decision.
Example
folder := tgcomp.FolderPicker(p.Main, "Output folder", &tgcomp.FolderPickerConf{
Write: true,
})
if folder != nil && tgcomp.Button(p.Main, "Export") {
if err := folder.MkdirAll("results"); err != nil {
return err
}
file, err := folder.Create("results/report.txt")
if err != nil {
return err
}
if _, err := io.WriteString(file, "Export complete\n"); err != nil {
_ = file.Close()
return err
}
return file.Close()
}