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