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