| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package dirprinter contains utilities for dumping the contents and structure |
| // of a directory tree. |
| package dirprinter |
| |
| import ( |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| const ( |
| // \u2502 is Box Drawings Light Vertical. |
| vertLine = '\u2502' |
| // \u2514 is Box Drawings Light Up And Right. |
| upRightLine = '\u2514' |
| // \u251c is Box Drawings Light Vertical And Right. |
| vertRightLine = '\u251c' |
| // \u2500 is Box Drawings Light Horizontal. |
| horizLine = '\u2500' |
| ) |
| |
| // computeLastChild traverses dir and returns a map containing the descendants |
| // of dir that are the last entry in their respective parents. |
| func computeLastChild(dir string) (map[string]bool, error) { |
| lastChild := make(map[string]string) |
| setChild := func(path string, _ os.FileInfo, _ error) error { |
| lastChild[filepath.Dir(path)] = path |
| return nil |
| } |
| if err := filepath.Walk(dir, setChild); err != nil { |
| return nil, err |
| } |
| isLastChild := make(map[string]bool) |
| for _, p := range lastChild { |
| isLastChild[p] = true |
| } |
| return isLastChild, nil |
| } |
| |
| // printDirTree traverses dir and pretty-prints the tree under dir. |
| func printDirTree(w io.Writer, dir string) error { |
| isLastChild, err := computeLastChild(dir) |
| if err != nil { |
| return err |
| } |
| printFn := func(path string, info os.FileInfo, err error) error { |
| me := path |
| var graphics []rune |
| prepend := func(p []rune, r ...rune) []rune { |
| return append(r, p...) |
| } |
| if err != nil { |
| // If the entry has an error, we want to print the error |
| // at the appropriate place in the tree (right below the |
| // entry, with the appropriate graphics preceding it). |
| me = filepath.Join(me, "dummy") |
| } |
| for me != dir { |
| parent, _ := filepath.Split(me) |
| me = filepath.Clean(parent) |
| if me == dir { |
| break |
| } |
| graphics = prepend(graphics, ' ', ' ') |
| if isLastChild[me] { |
| graphics = prepend(graphics, ' ') |
| } else { |
| graphics = prepend(graphics, vertLine) |
| } |
| } |
| if err != nil { |
| fmt.Fprint(w, string(graphics)) |
| fmt.Fprintf(w, "ERROR: %v\n", err) |
| return filepath.SkipDir |
| } |
| if filepath.Clean(path) != dir { |
| if isLastChild[path] { |
| graphics = append(graphics, upRightLine) |
| } else { |
| graphics = append(graphics, vertRightLine) |
| } |
| graphics = append(graphics, horizLine, horizLine) |
| } |
| fmt.Fprint(w, string(graphics)) |
| printEntry(w, info) |
| fmt.Fprint(w, "\n") |
| return nil |
| } |
| |
| return filepath.Walk(dir, printFn) |
| } |
| |
| // printEntry is used by printDirTree to show a summary of a directory entry. |
| func printEntry(w io.Writer, info os.FileInfo) { |
| fmt.Fprint(w, info.Name()) |
| if info.IsDir() { |
| fmt.Fprint(w, string(filepath.Separator)) |
| } else { |
| fmt.Fprintf(w, " %dB", info.Size()) |
| } |
| fmt.Fprintf(w, " %v", info.ModTime().Format("15:04:05.000000")) |
| } |
| |
| // printDirContents traverses the given dir and prints out information about |
| // descendant files. If the file is text, it also dumps the contents out. |
| func printDirContents(w io.Writer, dir string) error { |
| printFn := func(path string, info os.FileInfo, _ error) error { |
| if info.IsDir() { |
| return nil |
| } |
| path = filepath.Clean(path) |
| printPath, err := filepath.Rel(dir, path) |
| if err != nil { |
| return err |
| } |
| bannerSep := strings.Repeat(string(horizLine), len(printPath)) |
| fmt.Fprintln(w, bannerSep) |
| fmt.Fprintln(w, printPath) |
| fmt.Fprintln(w, bannerSep) |
| if err := dumpFile(w, path, info); err != nil { |
| fmt.Fprintf(w, "ERROR: %v\n", err) |
| } |
| return nil |
| } |
| return filepath.Walk(dir, printFn) |
| } |
| |
| // dumpFile prints out information about the file at path. |
| func dumpFile(w io.Writer, path string, info os.FileInfo) error { |
| switch { |
| case info.Mode().IsRegular(): |
| return dumpRegular(w, path) |
| case info.Mode()&os.ModeSymlink != 0: |
| target, err := filepath.EvalSymlinks(path) |
| if err != nil { |
| return err |
| } |
| fmt.Fprintf(w, "Link to: %s\n", target) |
| return nil |
| default: |
| fmt.Fprintf(w, "MODE: %x\n", info.Mode()) |
| return nil |
| } |
| } |
| |
| // dumpRegular prints out information about the regular file at path. If the |
| // content type is text, it dumps out the contents of the file. |
| func dumpRegular(w io.Writer, path string) error { |
| f, err := os.Open(path) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| data := make([]byte, 512) |
| count, err := io.ReadAtLeast(f, data, 512) |
| if err != nil && err != io.ErrUnexpectedEOF { |
| return err |
| } |
| data = data[:count] |
| contentType := http.DetectContentType(data) |
| fmt.Fprintln(w, "Content-type:", contentType) |
| if strings.HasPrefix(contentType, "text") { |
| if _, err := w.Write(data); err != nil { |
| return err |
| } |
| io.Copy(w, f) |
| fmt.Fprintln(w) |
| } |
| return nil |
| } |
| |
| // DumpDir dumps the dir's structure and files. |
| func DumpDir(w io.Writer, dir string) error { |
| dir = filepath.Clean(dir) |
| if err := printDirTree(w, dir); err != nil { |
| return err |
| } |
| return printDirContents(w, dir) |
| } |