fs.go (6292B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 // Package hugofs provides the file systems used by Hugo.
15 package hugofs
16
17 import (
18 "fmt"
19 "os"
20 "strings"
21
22 "github.com/bep/overlayfs"
23 "github.com/gohugoio/hugo/common/paths"
24 "github.com/gohugoio/hugo/config"
25 "github.com/spf13/afero"
26 )
27
28 // Os points to the (real) Os filesystem.
29 var Os = &afero.OsFs{}
30
31 // Fs holds the core filesystems used by Hugo.
32 type Fs struct {
33 // Source is Hugo's source file system.
34 // Note that this will always be a "plain" Afero filesystem:
35 // * afero.OsFs when running in production
36 // * afero.MemMapFs for many of the tests.
37 Source afero.Fs
38
39 // PublishDir is where Hugo publishes its rendered content.
40 // It's mounted inside publishDir (default /public).
41 PublishDir afero.Fs
42
43 // PublishDirStatic is the file system used for static files when --renderStaticToDisk is set.
44 // When this is set, PublishDir is set to write to memory.
45 PublishDirStatic afero.Fs
46
47 // PublishDirServer is the file system used for serving the public directory with Hugo's development server.
48 // This will typically be the same as PublishDir, but not if --renderStaticToDisk is set.
49 PublishDirServer afero.Fs
50
51 // Os is an OS file system.
52 // NOTE: Field is currently unused.
53 Os afero.Fs
54
55 // WorkingDirReadOnly is a read-only file system
56 // restricted to the project working dir.
57 WorkingDirReadOnly afero.Fs
58
59 // WorkingDirWritable is a writable file system
60 // restricted to the project working dir.
61 WorkingDirWritable afero.Fs
62 }
63
64 // NewDefault creates a new Fs with the OS file system
65 // as source and destination file systems.
66 func NewDefault(cfg config.Provider) *Fs {
67 fs := Os
68 return newFs(fs, fs, cfg)
69 }
70
71 // NewMem creates a new Fs with the MemMapFs
72 // as source and destination file systems.
73 // Useful for testing.
74 func NewMem(cfg config.Provider) *Fs {
75 fs := &afero.MemMapFs{}
76 return newFs(fs, fs, cfg)
77 }
78
79 // NewFrom creates a new Fs based on the provided Afero Fs
80 // as source and destination file systems.
81 // Useful for testing.
82 func NewFrom(fs afero.Fs, cfg config.Provider) *Fs {
83 return newFs(fs, fs, cfg)
84 }
85
86 // NewFrom creates a new Fs based on the provided Afero Fss
87 // as the source and destination file systems.
88 func NewFromSourceAndDestination(source, destination afero.Fs, cfg config.Provider) *Fs {
89 return newFs(source, destination, cfg)
90 }
91
92 func newFs(source, destination afero.Fs, cfg config.Provider) *Fs {
93 workingDir := cfg.GetString("workingDir")
94 publishDir := cfg.GetString("publishDir")
95 if publishDir == "" {
96 panic("publishDir is empty")
97 }
98
99 // Sanity check
100 if IsOsFs(source) && len(workingDir) < 2 {
101 panic("workingDir is too short")
102 }
103
104 absPublishDir := paths.AbsPathify(workingDir, publishDir)
105
106 // Make sure we always have the /public folder ready to use.
107 if err := source.MkdirAll(absPublishDir, 0777); err != nil && !os.IsExist(err) {
108 panic(err)
109 }
110
111 pubFs := afero.NewBasePathFs(destination, absPublishDir)
112
113 return &Fs{
114 Source: source,
115 PublishDir: pubFs,
116 PublishDirServer: pubFs,
117 PublishDirStatic: pubFs,
118 Os: &afero.OsFs{},
119 WorkingDirReadOnly: getWorkingDirFsReadOnly(source, workingDir),
120 WorkingDirWritable: getWorkingDirFsWritable(source, workingDir),
121 }
122 }
123
124 func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
125 if workingDir == "" {
126 return afero.NewReadOnlyFs(base)
127 }
128 return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir)
129 }
130
131 func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
132 if workingDir == "" {
133 return base
134 }
135 return afero.NewBasePathFs(base, workingDir)
136 }
137
138 func isWrite(flag int) bool {
139 return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
140 }
141
142 // MakeReadableAndRemoveAllModulePkgDir makes any subdir in dir readable and then
143 // removes the root.
144 // TODO(bep) move this to a more suitable place.
145 //
146 func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
147 // Safe guard
148 if !strings.Contains(dir, "pkg") {
149 panic(fmt.Sprint("invalid dir:", dir))
150 }
151
152 counter := 0
153 afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error {
154 if err != nil {
155 return nil
156 }
157 if info.IsDir() {
158 counter++
159 fs.Chmod(path, 0777)
160 }
161 return nil
162 })
163 return counter, fs.RemoveAll(dir)
164 }
165
166 // HasOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
167 // TODO(bep) make this nore robust.
168 func IsOsFs(fs afero.Fs) bool {
169 var isOsFs bool
170 WalkFilesystems(fs, func(fs afero.Fs) bool {
171 switch base := fs.(type) {
172 case *afero.MemMapFs:
173 isOsFs = false
174 case *afero.OsFs:
175 isOsFs = true
176 case *afero.BasePathFs:
177 _, supportsLstat, _ := base.LstatIfPossible("asdfasdfasdf")
178 isOsFs = supportsLstat
179 }
180 return isOsFs
181 })
182 return isOsFs
183 }
184
185 // FilesystemsUnwrapper returns the underlying filesystems.
186 type FilesystemsUnwrapper interface {
187 UnwrapFilesystems() []afero.Fs
188 }
189
190 // FilesystemsProvider returns the underlying filesystem.
191 type FilesystemUnwrapper interface {
192 UnwrapFilesystem() afero.Fs
193 }
194
195 // WalkFn is the walk func for WalkFilesystems.
196 type WalkFn func(fs afero.Fs) bool
197
198 // WalkFilesystems walks fs recursively and calls fn.
199 // If fn returns true, walking is stopped.
200 func WalkFilesystems(fs afero.Fs, fn WalkFn) bool {
201 if fn(fs) {
202 return true
203 }
204
205 if afs, ok := fs.(FilesystemUnwrapper); ok {
206 if WalkFilesystems(afs.UnwrapFilesystem(), fn) {
207 return true
208 }
209
210 } else if bfs, ok := fs.(FilesystemsUnwrapper); ok {
211 for _, sf := range bfs.UnwrapFilesystems() {
212 if WalkFilesystems(sf, fn) {
213 return true
214 }
215 }
216 } else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok {
217 for i := 0; i < cfs.NumFilesystems(); i++ {
218 if WalkFilesystems(cfs.Filesystem(i), fn) {
219 return true
220 }
221 }
222 }
223
224 return false
225 }