summaryrefslogtreecommitdiffstats
path: root/hugofs/fs.go
blob: 63c25a4c0d77decd82eaf9d9b1b7de1cc0eeb4fc (plain)
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package hugofs provides the file systems used by Hugo.
package hugofs

import (
	"fmt"
	"os"
	"strings"

	"github.com/bep/overlayfs"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/config"
	"github.com/spf13/afero"
)

// Os points to the (real) Os filesystem.
var Os = &afero.OsFs{}

// Fs holds the core filesystems used by Hugo.
type Fs struct {
	// Source is Hugo's source file system.
	// Note that this will always be a "plain" Afero filesystem:
	// * afero.OsFs when running in production
	// * afero.MemMapFs for many of the tests.
	Source afero.Fs

	// PublishDir is where Hugo publishes its rendered content.
	// It's mounted inside publishDir (default /public).
	PublishDir afero.Fs

	// PublishDirStatic is the file system used for static files  when --renderStaticToDisk is set.
	// When this is set, PublishDir is set to write to memory.
	PublishDirStatic afero.Fs

	// PublishDirServer is the file system used for serving the public directory with Hugo's development server.
	// This will typically be the same as PublishDir, but not if --renderStaticToDisk is set.
	PublishDirServer afero.Fs

	// Os is an OS file system.
	// NOTE: Field is currently unused.
	Os afero.Fs

	// WorkingDirReadOnly is a read-only file system
	// restricted to the project working dir.
	WorkingDirReadOnly afero.Fs

	// WorkingDirWritable is a writable file system
	// restricted to the project working dir.
	WorkingDirWritable afero.Fs
}

// NewDefault creates a new Fs with the OS file system
// as source and destination file systems.
func NewDefault(cfg config.Provider) *Fs {
	fs := Os
	return newFs(fs, fs, cfg)
}

// NewMem creates a new Fs with the MemMapFs
// as source and destination file systems.
// Useful for testing.
func NewMem(cfg config.Provider) *Fs {
	fs := &afero.MemMapFs{}
	return newFs(fs, fs, cfg)
}

// NewFrom creates a new Fs based on the provided Afero Fs
// as source and destination file systems.
// Useful for testing.
func NewFrom(fs afero.Fs, cfg config.Provider) *Fs {
	return newFs(fs, fs, cfg)
}

// NewFrom creates a new Fs based on the provided Afero Fss
// as the source and destination file systems.
func NewFromSourceAndDestination(source, destination afero.Fs, cfg config.Provider) *Fs {
	return newFs(source, destination, cfg)
}

func newFs(source, destination afero.Fs, cfg config.Provider) *Fs {
	workingDir := cfg.GetString("workingDir")
	publishDir := cfg.GetString("publishDir")
	if publishDir == "" {
		panic("publishDir is empty")
	}

	// Sanity check
	if IsOsFs(source) && len(workingDir) < 2 {
		panic("workingDir is too short")
	}

	absPublishDir := paths.AbsPathify(workingDir, publishDir)

	// Make sure we always have the /public folder ready to use.
	if err := source.MkdirAll(absPublishDir, 0777); err != nil && !os.IsExist(err) {
		panic(err)
	}

	pubFs := afero.NewBasePathFs(destination, absPublishDir)

	return &Fs{
		Source:             source,
		PublishDir:         pubFs,
		PublishDirServer:   pubFs,
		PublishDirStatic:   pubFs,
		Os:                 &afero.OsFs{},
		WorkingDirReadOnly: getWorkingDirFsReadOnly(source, workingDir),
		WorkingDirWritable: getWorkingDirFsWritable(source, workingDir),
	}
}

func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
	if workingDir == "" {
		return afero.NewReadOnlyFs(base)
	}
	return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir)
}

func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
	if workingDir == "" {
		return base
	}
	return afero.NewBasePathFs(base, workingDir)
}

func isWrite(flag int) bool {
	return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
}

// MakeReadableAndRemoveAllModulePkgDir makes any subdir in dir readable and then
// removes the root.
// TODO(bep) move this to a more suitable place.
//
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
	// Safe guard
	if !strings.Contains(dir, "pkg") {
		panic(fmt.Sprint("invalid dir:", dir))
	}

	counter := 0
	afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() {
			counter++
			fs.Chmod(path, 0777)
		}
		return nil
	})
	return counter, fs.RemoveAll(dir)
}

// HasOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
// TODO(bep) make this nore robust.
func IsOsFs(fs afero.Fs) bool {
	var isOsFs bool
	WalkFilesystems(fs, func(fs afero.Fs) bool {
		switch base := fs.(type) {
		case *afero.MemMapFs:
			isOsFs = false
		case *afero.OsFs:
			isOsFs = true
		case *afero.BasePathFs:
			_, supportsLstat, _ := base.LstatIfPossible("asdfasdfasdf")
			isOsFs = supportsLstat
		}
		return isOsFs
	})
	return isOsFs
}

// FilesystemsUnwrapper returns the underlying filesystems.
type FilesystemsUnwrapper interface {
	UnwrapFilesystems() []afero.Fs
}

// FilesystemsProvider returns the underlying filesystem.
type FilesystemUnwrapper interface {
	UnwrapFilesystem() afero.Fs
}

// WalkFn is the walk func for WalkFilesystems.
type WalkFn func(fs afero.Fs) bool

// WalkFilesystems walks fs recursively and calls fn.
// If fn returns true, walking is stopped.
func WalkFilesystems(fs afero.Fs, fn WalkFn) bool {
	if fn(fs) {
		return true
	}

	if afs, ok := fs.(FilesystemUnwrapper); ok {
		if WalkFilesystems(afs.UnwrapFilesystem(), fn) {
			return true
		}

	} else if bfs, ok := fs.(FilesystemsUnwrapper); ok {
		for _, sf := range bfs.UnwrapFilesystems() {
			if WalkFilesystems(sf, fn) {
				return true
			}
		}
	} else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok {
		for i := 0; i < cfs.NumFilesystems(); i++ {
			if WalkFilesystems(cfs.Filesystem(i), fn) {
				return true
			}
		}
	}

	return false
}