talos/pkg/xfs/root.go
Mateusz Urbanek 9db6dc06c3
feat: stop mounting state partition
Fixes #11608

Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
2025-09-18 15:34:28 +02:00

244 lines
5.2 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package xfs provides an extended file system interface that includes
// additional methods for writing files and directories, as well as utility
// functions for reading, writing, and manipulating files and directories
// within a specified file system.
package xfs
import (
"errors"
"fmt"
"io"
"io/fs"
"math/rand/v2"
"os"
"path/filepath"
"strconv"
"strings"
)
// FS is an interface for creating file system handles.
type FS interface {
Open() (int, error)
io.Closer
Repair() error
Source() string
FSType() string
}
// Root is an interface that extends the standard fs.FS interface with Write capabilities.
//
//nolint:interfacebloat
type Root interface {
fs.FS
io.Closer
OpenFS() error
RepairFS() error
Fd() (int, error)
Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flags int, perm os.FileMode) (File, error)
Remove(name string) error
Rename(oldname, newname string) error
Source() string
FSType() string
}
// File is an interface that extends the standard fs.File interface with additional methods for writing.
type File interface {
fs.File
io.Closer
io.Reader
io.ReaderAt
io.Seeker
io.Writer
io.WriterAt
Fd() uintptr
}
// Interface guard.
var _ File = (*os.File)(nil)
// ReadFile wraps fs.ReadFile to read a file from the specified FileSystem.
func ReadFile(root Root, name string) ([]byte, error) {
return fs.ReadFile(root, name)
}
// ReadDir wraps fs.ReadDir to read a directory from the specified FileSystem.
func ReadDir(root Root, name string) ([]fs.DirEntry, error) {
return fs.ReadDir(root, name)
}
// Stat wraps fs.Stat to get the file or directory information from the specified FileSystem.
func Stat(root Root, name string) (fs.FileInfo, error) {
return fs.Stat(root, name)
}
// WriteFile is equivalent of os.WriteFile acting on specified FileSystem.
func WriteFile(root Root, name string, data []byte, perm os.FileMode) error {
f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
// Open wraps (FS).Open.
func Open(root Root, name string) (fs.File, error) {
return root.Open(name)
}
// OpenFile wraps (FS).OpenFile.
func OpenFile(root Root, name string, flags int, perm os.FileMode) (File, error) {
return root.OpenFile(name, flags, perm)
}
// Mkdir wraps (FS).Mkdir.
func Mkdir(root Root, name string, perm os.FileMode) error {
return root.Mkdir(name, perm)
}
// MkdirAll is equivalent of os.MkdirAll acting on specified FileSystem.
func MkdirAll(root Root, name string, perm os.FileMode) error {
components := SplitPath(name)
for i := range len(components) + 1 {
dir := filepath.Join(components[:i]...)
if dir == "" {
// empty name, continue...
continue
}
err := root.Mkdir(dir, perm)
if err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("%w: %s", err, dir)
}
}
return nil
}
// MkdirTemp creates a temporary directory in the specified directory with a given pattern.
func MkdirTemp(root Root, dir, pattern string) (string, error) {
if dir == "" {
dir = os.TempDir()
}
if pattern == "" {
pattern = "tmp"
}
if strings.Count(pattern, "*") > 1 {
return "", fmt.Errorf("pattern %q must contain at most one '*'", pattern)
}
const maxAttempts = 10000
for range maxAttempts {
suffix := strconv.Itoa(rand.Int())
name := strings.Replace(pattern, "*", suffix, 1)
if !strings.Contains(pattern, "*") {
name = pattern + suffix
}
tempDir := filepath.Join(dir, name)
err := MkdirAll(root, tempDir, 0o777)
if err == nil {
return tempDir, nil
}
if errors.Is(err, fs.ErrExist) {
continue
}
return "", err
}
return "", fmt.Errorf("failed to create temporary directory after %d attempts", maxAttempts)
}
// Remove wraps (FS).Remove.
func Remove(root Root, name string) error {
return root.Remove(name)
}
// RemoveAll is equivalent of os.RemoveAll acting on specified FileSystem.
func RemoveAll(root Root, name string) (err error) {
var f fs.File
f, err = root.Open(name)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
stat, err := f.Stat()
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if !stat.IsDir() {
return root.Remove(name)
}
entries, err := ReadDir(root, name)
if err != nil {
return err
}
for _, entry := range entries {
childPath := filepath.Join(name, entry.Name())
if err := RemoveAll(root, childPath); err != nil {
return err
}
}
return root.Remove(name)
}
// Rename wraps (FS).Rename.
func Rename(root Root, oldname, newname string) error {
return root.Rename(oldname, newname)
}
// SplitPath splits a path into its components, similar to filepath.Split but returns all parts.
func SplitPath(path string) []string {
var parts []string
for {
dir, file := filepath.Split(path)
if file != "" {
parts = append([]string{file}, parts...)
}
if dir == "" || dir == path {
break
}
path = filepath.Clean(dir)
}
return parts
}