1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
// Copyright 2026 Rose Hogenson <r@roseh.moe>
//
// This file is part of ccp.
//
// Ccp is free software: you can redistribute it and/or modify it under the
// terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// Ccp is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// ccp. If not, see <https://www.gnu.org/licenses/>.
// Package wfs implements a "writable file system" in the spirit of [fs.FS].
package wfs
import (
"errors"
"io"
"io/fs"
"path"
)
// ReadLinkFS is backported from the latest go master.
type ReadLinkFS interface {
fs.FS
ReadLink(string) (string, error)
Lstat(string) (fs.FileInfo, error)
}
// ReadLink returns the destination of the named symbolic link.
//
// If fsys does not implement [ReadLinkFS], then ReadLink returns an error.
func ReadLink(fsys fs.FS, name string) (string, error) {
sym, ok := fsys.(ReadLinkFS)
if !ok {
return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid}
}
return sym.ReadLink(name)
}
// Lstat returns an [fs.FileInfo] describing the named file.
// If the file is a symbolic link, the returned [fs.FileInfo] describes the
// symbolic link. Lstat makes no attempt to follow the link.
//
// If fsys does not implement [ReadLinkFS], then Lstat is identical
// to [fs.Stat].
func Lstat(fsys fs.FS, name string) (fs.FileInfo, error) {
sym, ok := fsys.(ReadLinkFS)
if !ok {
return fs.Stat(fsys, name)
}
return sym.Lstat(name)
}
// An FS provides access to a writable hierarchical file system.
type FS interface {
fs.FS
Create(string, fs.FileMode) (io.WriteCloser, error)
Remove(string) error
Mkdir(string) error
Symlink(string, string) error
Chmod(string, fs.FileMode) error
}
// A MkdirModeFS is a file system with a mkdir method that accepts a file mode.
type MkdirModeFS interface {
FS
MkdirMode(string, fs.FileMode) error
}
// MkdirMode creates a directory with the given file permission. If fsys
// implements [MkdirModeFS], MkdirMode calls fsys.MkdirMode. Otherwise,
// MkdirMode calls Mkdir and then Chmod to set the mode.
func MkdirMode(fsys FS, name string, mode fs.FileMode) error {
if fsys, ok := fsys.(MkdirModeFS); ok {
return fsys.MkdirMode(name, mode)
}
if err := fsys.Mkdir(name); err != nil {
return err
}
return fsys.Chmod(name, mode)
}
func removeDir(fsys FS, dir string) error {
entries, readErr := fs.ReadDir(fsys, dir)
var err error
for _, d := range entries {
if err1 := removeAll(fsys, path.Join(dir, d.Name()), d); err == nil {
err = err1
}
}
if err == nil {
err = readErr
}
err1 := fsys.Remove(dir)
if err1 == nil || errors.Is(err, fs.ErrNotExist) {
return nil
}
if err == nil {
err = err1
}
return err
}
func removeAll(fsys FS, path string, d fs.DirEntry) error {
err := fsys.Remove(path)
if err == nil || errors.Is(err, fs.ErrNotExist) {
return nil
}
if !d.IsDir() {
return err
}
return removeDir(fsys, path)
}
// RemoveAll removes path and any children it contains. It removes everything it
// can but returns the first error it encounters. If the path does not exist,
// RemoveAll returns nil (no error).
func RemoveAll(fsys FS, path string) error {
// Simple case: if Remove works, we're done.
err := fsys.Remove(path)
if err == nil || errors.Is(err, fs.ErrNotExist) {
return nil
}
// Otherwise, is this a directory we need to recurse into?
dir, serr := Lstat(fsys, path)
if serr != nil {
if errors.Is(serr, fs.ErrNotExist) {
return nil
}
return serr
}
if !dir.IsDir() {
// Not a directory; return the error from Remove.
return err
}
return removeDir(fsys, path)
}
|