github.com/gohugoio/hugo@v0.88.1/hugofs/rootmapping_fs.go (about) 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 15 16 import ( 17 "fmt" 18 "os" 19 "path/filepath" 20 "strings" 21 22 "github.com/gohugoio/hugo/hugofs/files" 23 24 "github.com/pkg/errors" 25 26 radix "github.com/armon/go-radix" 27 "github.com/spf13/afero" 28 ) 29 30 var filepathSeparator = string(filepath.Separator) 31 32 // NewRootMappingFs creates a new RootMappingFs on top of the provided with 33 // root mappings with some optional metadata about the root. 34 // Note that From represents a virtual root that maps to the actual filename in To. 35 func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { 36 rootMapToReal := radix.New() 37 var virtualRoots []RootMapping 38 39 for _, rm := range rms { 40 (&rm).clean() 41 42 fromBase := files.ResolveComponentFolder(rm.From) 43 44 if len(rm.To) < 2 { 45 panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To)) 46 } 47 48 fi, err := fs.Stat(rm.To) 49 if err != nil { 50 if os.IsNotExist(err) { 51 continue 52 } 53 return nil, err 54 } 55 // Extract "blog" from "content/blog" 56 rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, fromBase), filepathSeparator) 57 if rm.Meta == nil { 58 rm.Meta = NewFileMeta() 59 } 60 61 rm.Meta.SourceRoot = rm.To 62 rm.Meta.BaseDir = rm.ToBasedir 63 rm.Meta.MountRoot = rm.path 64 rm.Meta.Module = rm.Module 65 66 meta := rm.Meta.Copy() 67 68 if !fi.IsDir() { 69 _, name := filepath.Split(rm.From) 70 meta.Name = name 71 } 72 73 rm.fi = NewFileMetaInfo(fi, meta) 74 75 key := filepathSeparator + rm.From 76 var mappings []RootMapping 77 v, found := rootMapToReal.Get(key) 78 if found { 79 // There may be more than one language pointing to the same root. 80 mappings = v.([]RootMapping) 81 } 82 mappings = append(mappings, rm) 83 rootMapToReal.Insert(key, mappings) 84 85 virtualRoots = append(virtualRoots, rm) 86 } 87 88 rootMapToReal.Insert(filepathSeparator, virtualRoots) 89 90 rfs := &RootMappingFs{ 91 Fs: fs, 92 rootMapToReal: rootMapToReal, 93 } 94 95 return rfs, nil 96 } 97 98 func newRootMappingFsFromFromTo( 99 baseDir string, 100 fs afero.Fs, 101 fromTo ...string, 102 ) (*RootMappingFs, error) { 103 rms := make([]RootMapping, len(fromTo)/2) 104 for i, j := 0, 0; j < len(fromTo); i, j = i+1, j+2 { 105 rms[i] = RootMapping{ 106 From: fromTo[j], 107 To: fromTo[j+1], 108 ToBasedir: baseDir, 109 } 110 } 111 112 return NewRootMappingFs(fs, rms...) 113 } 114 115 // RootMapping describes a virtual file or directory mount. 116 type RootMapping struct { 117 From string // The virtual mount. 118 To string // The source directory or file. 119 ToBasedir string // The base of To. May be empty if an absolute path was provided. 120 Module string // The module path/ID. 121 Meta *FileMeta // File metadata (lang etc.) 122 123 fi FileMetaInfo 124 path string // The virtual mount point, e.g. "blog". 125 126 } 127 128 type keyRootMappings struct { 129 key string 130 roots []RootMapping 131 } 132 133 func (rm *RootMapping) clean() { 134 rm.From = strings.Trim(filepath.Clean(rm.From), filepathSeparator) 135 rm.To = filepath.Clean(rm.To) 136 } 137 138 func (r RootMapping) filename(name string) string { 139 if name == "" { 140 return r.To 141 } 142 return filepath.Join(r.To, strings.TrimPrefix(name, r.From)) 143 } 144 145 // A RootMappingFs maps several roots into one. Note that the root of this filesystem 146 // is directories only, and they will be returned in Readdir and Readdirnames 147 // in the order given. 148 type RootMappingFs struct { 149 afero.Fs 150 rootMapToReal *radix.Tree 151 } 152 153 func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) { 154 base = filepathSeparator + fs.cleanName(base) 155 roots := fs.getRootsWithPrefix(base) 156 157 if roots == nil { 158 return nil, nil 159 } 160 161 fss := make([]FileMetaInfo, len(roots)) 162 for i, r := range roots { 163 bfs := afero.NewBasePathFs(fs.Fs, r.To) 164 bfs = decoratePath(bfs, func(name string) string { 165 p := strings.TrimPrefix(name, r.To) 166 if r.path != "" { 167 // Make sure it's mounted to a any sub path, e.g. blog 168 p = filepath.Join(r.path, p) 169 } 170 p = strings.TrimLeft(p, filepathSeparator) 171 return p 172 }) 173 fs := decorateDirs(bfs, r.Meta) 174 fi, err := fs.Stat("") 175 if err != nil { 176 return nil, errors.Wrap(err, "RootMappingFs.Dirs") 177 } 178 179 if !fi.IsDir() { 180 fi.(FileMetaInfo).Meta().Merge(r.Meta) 181 } 182 183 fss[i] = fi.(FileMetaInfo) 184 } 185 186 return fss, nil 187 } 188 189 // Filter creates a copy of this filesystem with only mappings matching a filter. 190 func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs { 191 rootMapToReal := radix.New() 192 fs.rootMapToReal.Walk(func(b string, v interface{}) bool { 193 rms := v.([]RootMapping) 194 var nrms []RootMapping 195 for _, rm := range rms { 196 if f(rm) { 197 nrms = append(nrms, rm) 198 } 199 } 200 if len(nrms) != 0 { 201 rootMapToReal.Insert(b, nrms) 202 } 203 return false 204 }) 205 206 fs.rootMapToReal = rootMapToReal 207 208 return &fs 209 } 210 211 // LstatIfPossible returns the os.FileInfo structure describing a given file. 212 func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { 213 fis, err := fs.doLstat(name) 214 if err != nil { 215 return nil, false, err 216 } 217 return fis[0], false, nil 218 } 219 220 // Open opens the named file for reading. 221 func (fs *RootMappingFs) Open(name string) (afero.File, error) { 222 fis, err := fs.doLstat(name) 223 if err != nil { 224 return nil, err 225 } 226 227 return fs.newUnionFile(fis...) 228 } 229 230 // Stat returns the os.FileInfo structure describing a given file. If there is 231 // an error, it will be of type *os.PathError. 232 func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) { 233 fi, _, err := fs.LstatIfPossible(name) 234 return fi, err 235 } 236 237 func (fs *RootMappingFs) hasPrefix(prefix string) bool { 238 hasPrefix := false 239 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v interface{}) bool { 240 hasPrefix = true 241 return true 242 }) 243 244 return hasPrefix 245 } 246 247 func (fs *RootMappingFs) getRoot(key string) []RootMapping { 248 v, found := fs.rootMapToReal.Get(key) 249 if !found { 250 return nil 251 } 252 253 return v.([]RootMapping) 254 } 255 256 func (fs *RootMappingFs) getRoots(key string) (string, []RootMapping) { 257 s, v, found := fs.rootMapToReal.LongestPrefix(key) 258 if !found || (s == filepathSeparator && key != filepathSeparator) { 259 return "", nil 260 } 261 return s, v.([]RootMapping) 262 } 263 264 func (fs *RootMappingFs) debug() { 265 fmt.Println("debug():") 266 fs.rootMapToReal.Walk(func(s string, v interface{}) bool { 267 fmt.Println("Key", s) 268 return false 269 }) 270 } 271 272 func (fs *RootMappingFs) getRootsWithPrefix(prefix string) []RootMapping { 273 var roots []RootMapping 274 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v interface{}) bool { 275 roots = append(roots, v.([]RootMapping)...) 276 return false 277 }) 278 279 return roots 280 } 281 282 func (fs *RootMappingFs) getAncestors(prefix string) []keyRootMappings { 283 var roots []keyRootMappings 284 fs.rootMapToReal.WalkPath(prefix, func(s string, v interface{}) bool { 285 if strings.HasPrefix(prefix, s+filepathSeparator) { 286 roots = append(roots, keyRootMappings{ 287 key: s, 288 roots: v.([]RootMapping), 289 }) 290 } 291 return false 292 }) 293 294 return roots 295 } 296 297 func (fs *RootMappingFs) newUnionFile(fis ...FileMetaInfo) (afero.File, error) { 298 meta := fis[0].Meta() 299 f, err := meta.Open() 300 if err != nil { 301 return nil, err 302 } 303 if len(fis) == 1 { 304 return f, nil 305 } 306 307 rf := &rootMappingFile{File: f, fs: fs, name: meta.Name, meta: meta} 308 if len(fis) == 1 { 309 return rf, err 310 } 311 312 next, err := fs.newUnionFile(fis[1:]...) 313 if err != nil { 314 return nil, err 315 } 316 317 uf := &afero.UnionFile{Base: rf, Layer: next} 318 319 uf.Merger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) { 320 // Ignore duplicate directory entries 321 seen := make(map[string]bool) 322 var result []os.FileInfo 323 324 for _, fis := range [][]os.FileInfo{bofi, lofi} { 325 for _, fi := range fis { 326 327 if fi.IsDir() && seen[fi.Name()] { 328 continue 329 } 330 331 if fi.IsDir() { 332 seen[fi.Name()] = true 333 } 334 335 result = append(result, fi) 336 } 337 } 338 339 return result, nil 340 } 341 342 return uf, nil 343 } 344 345 func (fs *RootMappingFs) cleanName(name string) string { 346 return strings.Trim(filepath.Clean(name), filepathSeparator) 347 } 348 349 func (fs *RootMappingFs) collectDirEntries(prefix string) ([]os.FileInfo, error) { 350 prefix = filepathSeparator + fs.cleanName(prefix) 351 352 var fis []os.FileInfo 353 354 seen := make(map[string]bool) // Prevent duplicate directories 355 level := strings.Count(prefix, filepathSeparator) 356 357 collectDir := func(rm RootMapping, fi FileMetaInfo) error { 358 f, err := fi.Meta().Open() 359 if err != nil { 360 return err 361 } 362 direntries, err := f.Readdir(-1) 363 if err != nil { 364 f.Close() 365 return err 366 } 367 368 for _, fi := range direntries { 369 meta := fi.(FileMetaInfo).Meta() 370 meta.Merge(rm.Meta) 371 if fi.IsDir() { 372 name := fi.Name() 373 if seen[name] { 374 continue 375 } 376 seen[name] = true 377 opener := func() (afero.File, error) { 378 return fs.Open(filepath.Join(rm.From, name)) 379 } 380 fi = newDirNameOnlyFileInfo(name, meta, opener) 381 } 382 383 fis = append(fis, fi) 384 } 385 386 f.Close() 387 388 return nil 389 } 390 391 // First add any real files/directories. 392 rms := fs.getRoot(prefix) 393 for _, rm := range rms { 394 if err := collectDir(rm, rm.fi); err != nil { 395 return nil, err 396 } 397 } 398 399 // Next add any file mounts inside the given directory. 400 prefixInside := prefix + filepathSeparator 401 fs.rootMapToReal.WalkPrefix(prefixInside, func(s string, v interface{}) bool { 402 if (strings.Count(s, filepathSeparator) - level) != 1 { 403 // This directory is not part of the current, but we 404 // need to include the first name part to make it 405 // navigable. 406 path := strings.TrimPrefix(s, prefixInside) 407 parts := strings.Split(path, filepathSeparator) 408 name := parts[0] 409 410 if seen[name] { 411 return false 412 } 413 seen[name] = true 414 opener := func() (afero.File, error) { 415 return fs.Open(path) 416 } 417 418 fi := newDirNameOnlyFileInfo(name, nil, opener) 419 fis = append(fis, fi) 420 421 return false 422 } 423 424 rms := v.([]RootMapping) 425 for _, rm := range rms { 426 if !rm.fi.IsDir() { 427 // A single file mount 428 fis = append(fis, rm.fi) 429 continue 430 } 431 name := filepath.Base(rm.From) 432 if seen[name] { 433 continue 434 } 435 seen[name] = true 436 437 opener := func() (afero.File, error) { 438 return fs.Open(rm.From) 439 } 440 441 fi := newDirNameOnlyFileInfo(name, rm.Meta, opener) 442 443 fis = append(fis, fi) 444 445 } 446 447 return false 448 }) 449 450 // Finally add any ancestor dirs with files in this directory. 451 ancestors := fs.getAncestors(prefix) 452 for _, root := range ancestors { 453 subdir := strings.TrimPrefix(prefix, root.key) 454 for _, rm := range root.roots { 455 if rm.fi.IsDir() { 456 fi, err := rm.fi.Meta().JoinStat(subdir) 457 if err == nil { 458 if err := collectDir(rm, fi); err != nil { 459 return nil, err 460 } 461 } 462 } 463 } 464 } 465 466 return fis, nil 467 } 468 469 func (fs *RootMappingFs) doLstat(name string) ([]FileMetaInfo, error) { 470 name = fs.cleanName(name) 471 key := filepathSeparator + name 472 473 roots := fs.getRoot(key) 474 475 if roots == nil { 476 if fs.hasPrefix(key) { 477 // We have directories mounted below this. 478 // Make it look like a directory. 479 return []FileMetaInfo{newDirNameOnlyFileInfo(name, nil, fs.virtualDirOpener(name))}, nil 480 } 481 482 // Find any real files or directories with this key. 483 _, roots := fs.getRoots(key) 484 if roots == nil { 485 return nil, &os.PathError{Op: "LStat", Path: name, Err: os.ErrNotExist} 486 } 487 488 var err error 489 var fis []FileMetaInfo 490 491 for _, rm := range roots { 492 var fi FileMetaInfo 493 fi, _, err = fs.statRoot(rm, name) 494 if err == nil { 495 fis = append(fis, fi) 496 } 497 } 498 499 if fis != nil { 500 return fis, nil 501 } 502 503 if err == nil { 504 err = &os.PathError{Op: "LStat", Path: name, Err: err} 505 } 506 507 return nil, err 508 } 509 510 fileCount := 0 511 for _, root := range roots { 512 if !root.fi.IsDir() { 513 fileCount++ 514 } 515 if fileCount > 1 { 516 break 517 } 518 } 519 520 if fileCount == 0 { 521 // Dir only. 522 return []FileMetaInfo{newDirNameOnlyFileInfo(name, roots[0].Meta, fs.virtualDirOpener(name))}, nil 523 } 524 525 if fileCount > 1 { 526 // Not supported by this filesystem. 527 return nil, errors.Errorf("found multiple files with name %q, use .Readdir or the source filesystem directly", name) 528 } 529 530 return []FileMetaInfo{roots[0].fi}, nil 531 } 532 533 func (fs *RootMappingFs) statRoot(root RootMapping, name string) (FileMetaInfo, bool, error) { 534 filename := root.filename(name) 535 536 fi, b, err := lstatIfPossible(fs.Fs, filename) 537 if err != nil { 538 return nil, b, err 539 } 540 541 var opener func() (afero.File, error) 542 if fi.IsDir() { 543 // Make sure metadata gets applied in Readdir. 544 opener = fs.realDirOpener(filename, root.Meta) 545 } else { 546 // Opens the real file directly. 547 opener = func() (afero.File, error) { 548 return fs.Fs.Open(filename) 549 } 550 } 551 552 return decorateFileInfo(fi, fs.Fs, opener, "", "", root.Meta), b, nil 553 } 554 555 func (fs *RootMappingFs) virtualDirOpener(name string) func() (afero.File, error) { 556 return func() (afero.File, error) { return &rootMappingFile{name: name, fs: fs}, nil } 557 } 558 559 func (fs *RootMappingFs) realDirOpener(name string, meta *FileMeta) func() (afero.File, error) { 560 return func() (afero.File, error) { 561 f, err := fs.Fs.Open(name) 562 if err != nil { 563 return nil, err 564 } 565 return &rootMappingFile{name: name, meta: meta, fs: fs, File: f}, nil 566 } 567 } 568 569 type rootMappingFile struct { 570 afero.File 571 fs *RootMappingFs 572 name string 573 meta *FileMeta 574 } 575 576 func (f *rootMappingFile) Close() error { 577 if f.File == nil { 578 return nil 579 } 580 return f.File.Close() 581 } 582 583 func (f *rootMappingFile) Name() string { 584 return f.name 585 } 586 587 func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) { 588 if f.File != nil { 589 fis, err := f.File.Readdir(count) 590 if err != nil { 591 return nil, err 592 } 593 594 for i, fi := range fis { 595 fis[i] = decorateFileInfo(fi, f.fs, nil, "", "", f.meta) 596 } 597 return fis, nil 598 } 599 return f.fs.collectDirEntries(f.name) 600 } 601 602 func (f *rootMappingFile) Readdirnames(count int) ([]string, error) { 603 dirs, err := f.Readdir(count) 604 if err != nil { 605 return nil, err 606 } 607 return fileInfosToNames(dirs), nil 608 }