From 3fef0c79db4f097d131dbcdcb5a82b947c807922 Mon Sep 17 00:00:00 2001 From: Angelo 'Flecart' Huang Date: Thu, 16 Jun 2022 17:18:57 +0200 Subject: [PATCH 01/16] refactor: working version before modifying generate --- statik.go | 430 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 315 insertions(+), 115 deletions(-) diff --git a/statik.go b/statik.go index d0fa735..9f38efd 100644 --- a/statik.go +++ b/statik.go @@ -3,6 +3,7 @@ package main import ( "bytes" _ "embed" + "errors" "flag" "html/template" "io" @@ -25,6 +26,29 @@ import ( "github.com/tdewolff/minify/v2/js" ) +// Describes the state of every main variable of the program +type ProgramState struct { + isRecursive bool + isEmpty bool + enableSort bool + convertLink bool + + styleTemplate string + footerTemplate string + headerTemplate string + lineTemplate string + + srcDir string + destDir string + + includeRegEx *regexp.Regexp + excludeRegEx *regexp.Regexp + includeRegExStr string + excludeRegExStr string + URL string + baseURL *url.URL +} + type Dir struct { Name string URL string @@ -48,16 +72,128 @@ type Line struct { Size string Date time.Time } +type Directory struct { + Path string `json:"path"` + Name string `json:"name"` + Directories []Directory `json:"directories"` + Files []File `json:"files"` +} -const linkSuffix = ".link" +type FuzzyFile struct { + Path string `json:"path"` + Name string `json:"name"` + Mime string `json:"mime"` +} -var ( - baseDir, outDir string - baseURL *url.URL = nil +type File struct { + FuzzyFile + Size uint64 `json:"size"` + ModTime time.Time `json:"time"` +} + +// WARNING: don't call this with directory FileInfo, not supported +func getFile(file os.FileInfo, path string) File { + return File{ + FuzzyFile: FuzzyFile{ + Path: path, + Name: file.Name(), + Mime: "tmp", // TODO: make a function that returns the correct mime + }, + Size: uint64(file.Size()), + ModTime: file.ModTime(), + } +} + +// WARNING: don't call this with FileInfo that is not a directory, not supported +func getDirectory(file os.FileInfo, path string) Directory { + return Directory{ + path, + file.Name(), + []Directory{}, + []File{}, + } +} + +// Separates files and directories +func unpackFiles(fileInfo []os.FileInfo) ([]os.FileInfo, []os.FileInfo) { + var files []os.FileInfo + var dirs []os.FileInfo + for _, file := range fileInfo { + if !file.IsDir() { + files = append(files, file) + } else { + dirs = append(dirs, file) + } + } + return files, dirs +} + +func IsPathValid(path string) error { + dir, err := os.Stat(path) + if err != nil { + return err + } + if !dir.IsDir() { + return errors.New("the given path does not correspond to a directory") + } + return nil +} + +func GetDirectoryStructure(path string, recursive bool, directory *Directory) error { + err := IsPathValid(path) + if err != nil { + return err + } + + // fill unitialized directory value (usually in first call) + if directory.Name == "" { + file, _ := os.Stat(path) + *directory = getDirectory(file, path) + } + + filesInDir, err := ioutil.ReadDir(path) + if err != nil { + return err + } + + files, dirs := unpackFiles(filesInDir) + for _, file := range files { + directory.Files = append(directory.Files, getFile(file, path)) + } - include, exclude *regexp.Regexp = nil, nil - empty, recursive, sortEntries, converLinks bool + for _, dir := range dirs { + directory.Directories = append(directory.Directories, getDirectory(dir, path)) + } + if recursive { + for idx, dir := range directory.Directories { + dirName := filepath.Join(path, dir.Name) + err := GetDirectoryStructure(dirName, true, &directory.Directories[idx]) + if err != nil { + return err + } + } + } + return nil +} + +// used to handle the template programming +// every gohtml template should implement this interface +// TODO: this template interface could fit well with Registrary +// design pattern: everytime you have to use a new template +// you just register it! +type Template interface { + Data(interface{}) interface{} // the interface that this template builds upon + Load(string) // load the teplate + Raw() string // the default filepath for the template + Tmpl() *template.Template // return the actual template +} + +const linkSuffix = ".link" +const defaultSrc = "./" +const defaultDest = "site" + +var ( //go:embed "style.css" style string //go:embed "header.gohtml" @@ -71,11 +207,11 @@ var ( ) // joins the baseUrl path with the given relative path and returns the url as a string -func withBaseURL(rel string) string { - cpy := baseURL.Path - baseURL.Path = path.Join(baseURL.Path, rel) - res := baseURL.String() - baseURL.Path = cpy +func withBaseURL(state *ProgramState, rel string) string { + cpy := state.baseURL.Path + state.baseURL.Path = path.Join(state.baseURL.Path, rel) + res := state.baseURL.String() + state.baseURL.Path = cpy return res } @@ -88,7 +224,7 @@ func gen(tmpl *template.Template, data interface{}, out io.Writer) { func copy(src, dest string) { input, err := ioutil.ReadFile(src) if err != nil { - log.Fatalf("Could not open source file for copying: %s\n%s\n", src, err) + log.Fatalf("Could not open srcDir file for copying: %s\n%s\n", src, err) } err = ioutil.WriteFile(dest, input, 0644) if err != nil { @@ -96,40 +232,54 @@ func copy(src, dest string) { } } -func filter(entries []fs.FileInfo) []fs.FileInfo { +func filter(state *ProgramState, entries []fs.FileInfo) []fs.FileInfo { filtered := []fs.FileInfo{} for _, entry := range entries { - if entry.IsDir() && !exclude.MatchString(entry.Name()) || (!entry.IsDir() && include.MatchString(entry.Name()) && !exclude.MatchString(entry.Name())) { + if entry.IsDir() && !state.excludeRegEx.MatchString(entry.Name()) || + (!entry.IsDir() && state.includeRegEx.MatchString(entry.Name()) && + !state.excludeRegEx.MatchString(entry.Name())) { filtered = append(filtered, entry) } } return filtered } -func generate(m *minify.M, dir string, parts []string) bool { - entries, err := ioutil.ReadDir(dir) +// SECTION CLARITY: remove this later, just tmp for clarity +// sort by isDirectory and alphabetical naming +func sortAlphabetically(files []os.FileInfo) { + sort.Slice(files, func(i, j int) bool { + isFirstEntryDir := files[i].IsDir() + isSecondEntryDir := files[j].IsDir() + return isFirstEntryDir && !isSecondEntryDir || + (isFirstEntryDir || !isSecondEntryDir) && + files[i].Name() < files[j].Name() + }) +} + +func generate(state *ProgramState, m *minify.M, dirName string, parts []string) bool { + filesInDir, err := ioutil.ReadDir(dirName) if err != nil { - log.Fatalf("Could not read input directory: %s\n%s\n", dir, err) + log.Fatalf("Could not read input directory: %s\n%s\n", dirName, err) } - entries = filter(entries) - if len(entries) == 0 { - return empty + + filteredFiles := filter(state, filesInDir) + if len(filteredFiles) == 0 { + return state.isEmpty } - if sortEntries { - sort.Slice(entries, func(i, j int) bool { - isFirstEntryDir := entries[i].IsDir() - isSecondEntryDir := entries[j].IsDir() - return isFirstEntryDir && !isSecondEntryDir || - (isFirstEntryDir || !isSecondEntryDir) && - entries[i].Name() < entries[j].Name() - }) + + if state.enableSort { + sortAlphabetically(filteredFiles) } + // CHECK IF OUTPUT DIRECTORY HAS GOOD PERMS rel := path.Join(parts...) - outDir := path.Join(outDir, rel) - if err := os.Mkdir(outDir, os.ModePerm); err != nil { + outDir := path.Join(state.destDir, rel) + err = os.Mkdir(outDir, os.ModePerm) + if err != nil { log.Fatalf("Could not create output *sub*directory: %s\n%s\n", outDir, err) } + + // LOAD HTML htmlPath := path.Join(outDir, "index.html") html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, 0666) if err != nil { @@ -137,37 +287,51 @@ func generate(m *minify.M, dir string, parts []string) bool { } out := new(bytes.Buffer) + // TRACKER: variabili esterne usate: + // parts + // baseURL // Generate the header and the double dots back anchor when appropriate { p, url := []Dir{}, "" for _, part := range parts { url = path.Join(url, part) - p = append(p, Dir{Name: part, URL: withBaseURL(url)}) + p = append(p, Dir{Name: part, URL: withBaseURL(state, url)}) } gen(header, Header{ Root: Dir{ - Name: strings.TrimPrefix(strings.TrimSuffix(baseURL.Path, "/"), "/"), - URL: baseURL.String(), + Name: strings.TrimPrefix(strings.TrimSuffix(state.baseURL.Path, "/"), "/"), + URL: state.baseURL.String(), }, Parts: p, - FullPath: path.Join(baseURL.Path+rel) + "/", + FullPath: path.Join(state.baseURL.Path+rel) + "/", Stylesheet: template.CSS(style), }, out) } - // Populte the back line + + // TRACKER: variabili esterne usate: + // rel + // Populate the back line { if len(parts) != 0 { gen(line, Line{ IsDir: true, Name: "..", - URL: withBaseURL(path.Join(rel, "..")), + URL: withBaseURL(state, path.Join(rel, "..")), Size: humanize.Bytes(0), }, out) } } - for _, entry := range entries { - pth := path.Join(dir, entry.Name()) + // TRACKER: variabili esterne usate: + // filteredFiles + // rel + // dirName + // outDir + // linkSuffix + // line + // state ProgramState + for _, entry := range filteredFiles { + pth := path.Join(dirName, entry.Name()) // Avoid recursive infinite loop if pth == outDir { continue @@ -176,12 +340,13 @@ func generate(m *minify.M, dir string, parts []string) bool { data := Line{ IsDir: entry.IsDir(), Name: entry.Name(), - URL: withBaseURL(path.Join(rel, entry.Name())), + URL: withBaseURL(state, path.Join(rel, entry.Name())), Size: humanize.Bytes(uint64(entry.Size())), Date: entry.ModTime(), } + if strings.HasSuffix(pth, linkSuffix) { - data.Name = data.Name[:len(data.Name)-len(linkSuffix)] + data.Name = data.Name[:len(data.Name)-len(linkSuffix)] // get name without extension data.Size = humanize.Bytes(0) raw, err := ioutil.ReadFile(pth) @@ -199,8 +364,9 @@ func generate(m *minify.M, dir string, parts []string) bool { continue } + // TODO: simplify this logic // Only list directories when recursing and only those which are not empty - if !entry.IsDir() || recursive && generate(m, pth, append(parts, entry.Name())) { + if !entry.IsDir() || state.isRecursive && generate(state, m, pth, append(parts, entry.Name())) { gen(line, data, out) } @@ -210,15 +376,17 @@ func generate(m *minify.M, dir string, parts []string) bool { } } gen(footer, Footer{Date: time.Now()}, out) - if err := m.Minify("text/html", html, out); err != nil { + err = m.Minify("text/html", html, out) + if err != nil { log.Fatalf("Could not write to index.html: %s\n%s\n", htmlPath, err) } - if err := html.Close(); err != nil { + err = html.Close() + if err != nil { log.Fatalf("Could not write to close index.html: %s\n%s\n", htmlPath, err) } - log.Printf("Generated data for directory: %s\n", dir) + log.Printf("Generated data for directory: %s\n", dirName) - return !empty + return !state.isEmpty } func loadTemplate(name string, path string, def *string, dest **template.Template) { @@ -227,104 +395,136 @@ func loadTemplate(name string, path string, def *string, dest **template.Templat err error ) if path != "" { - if content, err = ioutil.ReadFile(path); err != nil { + content, err = ioutil.ReadFile(path) + if err != nil { log.Fatalf("Could not read %s template file %s:\n%s\n", name, path, err) } *def = string(content) } - if *dest, err = template.New(name).Parse(*def); err != nil { - log.Fatalf("Could not parse %s template:\n%s\n", name, path, err) + *dest, err = template.New(name).Parse(*def) + if err != nil { + log.Fatalf("Could not parse %s template %s:\n%s\n", name, path, err) } } -func main() { - i := flag.String("i", ".*", "A regex pattern to include files into the listing") - e := flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") - r := flag.Bool("r", true, "Recursively scan the file tree") - emp := flag.Bool("empty", false, "Whether to list empty directories") - s := flag.Bool("sort", true, "Sort files A-z and by type") - b := flag.String("b", "http://localhost", "The base URL") - l := flag.Bool("l", false, "Convert .link files to anchor tags") - argstyle := flag.String("style", "", "Use a custom stylesheet file") - argfooter := flag.String("footer", "", "Use a custom footer template") - argheader := flag.String("header", "", "Use a custom header template") - argline := flag.String("line", "", "Use a custom line template") +func logState(state *ProgramState) { + log.Println("Running with parameters:") + log.Println("\tInclude:\t", state.includeRegExStr) + log.Println("\tExclude:\t", state.excludeRegExStr) + log.Println("\tRecursive:\t", state.isRecursive) + log.Println("\tEmpty:\t\t", state.isEmpty) + log.Println("\tConvert links:\t", state.convertLink) + log.Println("\tSource:\t\t", state.srcDir) + log.Println("\tDestination:\t", state.destDir) + log.Println("\tBase URL:\t", state.URL) + log.Println("\tStyle:\t\t", state.styleTemplate) + log.Println("\tFooter:\t\t", state.footerTemplate) + log.Println("\tHeader:\t\t", state.headerTemplate) + log.Println("\tline:\t\t", state.lineTemplate) +} + +func getAbsolutePath(filePath string) string { + if !filepath.IsAbs(filePath) { + wd, err := os.Getwd() + if err != nil { + log.Fatal("Could not get currently working directory", err) + } + return path.Join(wd, filePath) + } else { + return filePath + } +} + +// remove all files from input directory +func clearDirectory(filePath string) { + _, err := os.Stat(filePath) + if err == nil { + err = os.RemoveAll(filePath) + if err != nil { + log.Fatalf("Could not remove output directory previous contents: %s\n%s\n", filePath, err) + } + } +} + +// handles every input parameter of the Program, returns it in ProgramState. +// if something its wrong, the whole program just panick-exits +func initProgram(state *ProgramState) { + state.includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") + state.excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") + state.isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") + state.isEmpty = *flag.Bool("empty", false, "Whether to list empty directories") + state.enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") + state.URL = *flag.String("b", "http://localhost", "The base URL") + state.convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") + state.styleTemplate = *flag.String("style", "", "Use a custom stylesheet file") + state.footerTemplate = *flag.String("footer", "", "Use a custom footer template") + state.headerTemplate = *flag.String("header", "", "Use a custom header template") + state.lineTemplate = *flag.String("line", "", "Use a custom line template") + state.srcDir = defaultSrc + state.destDir = defaultDest flag.Parse() args := flag.Args() - src, dest := ".", "site" if len(args) > 2 { - log.Fatal("Invalid number of aruments, expected two at max") + log.Fatal("Invalid number of arguments, expected two at max (source and dest)") } if len(args) == 1 { - dest = args[0] + state.destDir = args[0] } else if len(args) == 2 { - src = args[0] - dest = args[1] + state.srcDir = args[0] + state.destDir = args[1] } - log.Println("Running with parameters:") - log.Println("\tInclude:\t", *i) - log.Println("\tExclude:\t", *e) - log.Println("\tRecursive:\t", *r) - log.Println("\tEmpty:\t\t", *emp) - log.Println("\tConvert links:\t", *l) - log.Println("\tSource:\t\t", src) - log.Println("\tDestination:\t", dest) - log.Println("\tBase URL:\t", *b) - log.Println("\tStyle:\t\t", *argstyle) - log.Println("\tFooter:\t\t", *argfooter) - log.Println("\tHeader:\t\t", *argheader) - log.Println("\tline:\t\t", *argline) + + // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose + logState(state) + state.srcDir = getAbsolutePath(state.srcDir) + state.destDir = getAbsolutePath(state.destDir) + clearDirectory(state.destDir) var err error - if include, err = regexp.Compile(*i); err != nil { + state.includeRegEx, err = regexp.Compile(state.includeRegExStr) + if err != nil { log.Fatal("Invalid regexp for include matching", err) } - if exclude, err = regexp.Compile(*e); err != nil { + state.excludeRegEx, err = regexp.Compile(state.excludeRegExStr) + if err != nil { log.Fatal("Invalid regexp for exclude matching", err) } - recursive = *r - empty = *emp - sortEntries = *s - converLinks = *l - var wd string - if !filepath.IsAbs(src) || !filepath.IsAbs(dest) { - wd, err = os.Getwd() - if err != nil { - log.Fatal("Could not get currently working directory", err) - } - } - if baseDir = src; !filepath.IsAbs(src) { - baseDir = path.Join(wd, src) - } - if outDir = dest; !filepath.IsAbs(dest) { - outDir = path.Join(wd, dest) - } - if _, err := os.Stat(outDir); err == nil { - if err = os.RemoveAll(outDir); err != nil { - log.Fatalf("Could not remove output directory previous contents: %s\n%s\n", outDir, err) - } - } - if baseURL, err = url.Parse(*b); err != nil { - log.Fatalf("Could not parse base URL: %s\n%s\n", *b, err) + state.baseURL, err = url.Parse(state.URL) + if err != nil { + log.Fatalf("Could not parse base URL: %s\n%s\n", state.URL, err) } - loadTemplate("header", *argheader, &rawHeader, &header) - loadTemplate("line", *argline, &rawLine, &line) - loadTemplate("footer", *argfooter, &rawFooter, &footer) + // TODO: use the registry design pattern to generalize the template loading, parsing and execution + // This section should not belong to initProgram because it doesnt modify things on ProgramState, + // just needs access + loadTemplate("header", state.headerTemplate, &rawHeader, &header) + loadTemplate("line", state.lineTemplate, &rawLine, &line) + loadTemplate("footer", state.footerTemplate, &rawFooter, &footer) - if *argstyle != "" { + if state.styleTemplate != "" { var content []byte - if content, err = ioutil.ReadFile(*argstyle); err != nil { - log.Fatalf("Could not read stylesheet file %s:\n%s\n", *argstyle, err) + if content, err = ioutil.ReadFile(state.styleTemplate); err != nil { + log.Fatalf("Could not read stylesheet file %s:\n%s\n", state.styleTemplate, err) } style = string(content) } +} + +func main() { + var state ProgramState + var srcStructure Directory + initProgram(&state) + + err := GetDirectoryStructure(state.srcDir, state.isRecursive, &srcStructure) + if err != nil { + log.Fatalf("Error when creating the directory structure:\n%s\n", err) + } - m := minify.New() - m.AddFunc("text/css", css.Minify) - m.AddFunc("text/html", html.Minify) - m.AddFunc("application/javascript", js.Minify) - generate(m, baseDir, []string{}) + minifier := minify.New() + minifier.AddFunc("text/css", css.Minify) + minifier.AddFunc("text/html", html.Minify) + minifier.AddFunc("application/javascript", js.Minify) + generate(&state, minifier, state.srcDir, []string{}) } From 69808e2cb4ba469da69aaa11ea034d9af9d43ec0 Mon Sep 17 00:00:00 2001 From: Angelo 'Flecart' Huang Date: Thu, 16 Jun 2022 19:51:47 +0200 Subject: [PATCH 02/16] refactor: refactored the generate function successfully --- statik.go | 449 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 252 insertions(+), 197 deletions(-) diff --git a/statik.go b/statik.go index 9f38efd..4f3bcb0 100644 --- a/statik.go +++ b/statik.go @@ -7,7 +7,6 @@ import ( "flag" "html/template" "io" - "io/fs" "io/ioutil" "log" "net/url" @@ -28,25 +27,29 @@ import ( // Describes the state of every main variable of the program type ProgramState struct { - isRecursive bool - isEmpty bool - enableSort bool - convertLink bool - - styleTemplate string - footerTemplate string - headerTemplate string - lineTemplate string - - srcDir string - destDir string - - includeRegEx *regexp.Regexp - excludeRegEx *regexp.Regexp - includeRegExStr string - excludeRegExStr string + IsRecursive bool + IsEmpty bool + EnableSort bool + ConvertLink bool + + // TODO: i have to convert these template string in other form, so that i can generalize the + // generation + StyleTemplate string + FooterTemplate string + HeaderTemplate string + LineTemplate string + + SrcDir string + DestDir string + + IncludeRegEx *regexp.Regexp + ExcludeRegEx *regexp.Regexp + IncludeRegExStr string + ExcludeRegExStr string URL string - baseURL *url.URL + BaseURL *url.URL + + Minifier *minify.M } type Dir struct { @@ -72,11 +75,28 @@ type Line struct { Size string Date time.Time } + +// this interface will be used to handle the template programming +// every gohtml template should implement this interface +// TODO: this template interface could fit well with Registrary +// design pattern: everytime you have to use a new template +// you just register it (aka implement needed functions)! +// PROBLEM: i don't have any idea how to make it, i don't even know +// if it could be a good choice +type Template interface { + Data(interface{}) interface{} // the interface that this template builds upon + Load(string) // load the teplate + Raw() string // the default filepath for the template + Tmpl() *template.Template // just return the template pointer +} + type Directory struct { Path string `json:"path"` Name string `json:"name"` Directories []Directory `json:"directories"` Files []File `json:"files"` + Size uint64 `json:"size"` + ModTime time.Time `json:"time"` } type FuzzyFile struct { @@ -91,6 +111,10 @@ type File struct { ModTime time.Time `json:"time"` } +const linkSuffix = ".link" +const defaultSrc = "./" +const defaultDest = "site" + // WARNING: don't call this with directory FileInfo, not supported func getFile(file os.FileInfo, path string) File { return File{ @@ -107,10 +131,12 @@ func getFile(file os.FileInfo, path string) File { // WARNING: don't call this with FileInfo that is not a directory, not supported func getDirectory(file os.FileInfo, path string) Directory { return Directory{ - path, - file.Name(), - []Directory{}, - []File{}, + Path: path, + Name: file.Name(), + Directories: []Directory{}, + Files: []File{}, + Size: uint64(file.Size()), + ModTime: file.ModTime(), } } @@ -145,12 +171,6 @@ func GetDirectoryStructure(path string, recursive bool, directory *Directory) er return err } - // fill unitialized directory value (usually in first call) - if directory.Name == "" { - file, _ := os.Stat(path) - *directory = getDirectory(file, path) - } - filesInDir, err := ioutil.ReadDir(path) if err != nil { return err @@ -177,22 +197,6 @@ func GetDirectoryStructure(path string, recursive bool, directory *Directory) er return nil } -// used to handle the template programming -// every gohtml template should implement this interface -// TODO: this template interface could fit well with Registrary -// design pattern: everytime you have to use a new template -// you just register it! -type Template interface { - Data(interface{}) interface{} // the interface that this template builds upon - Load(string) // load the teplate - Raw() string // the default filepath for the template - Tmpl() *template.Template // return the actual template -} - -const linkSuffix = ".link" -const defaultSrc = "./" -const defaultDest = "site" - var ( //go:embed "style.css" style string @@ -206,12 +210,12 @@ var ( header, footer, line *template.Template ) -// joins the baseUrl path with the given relative path and returns the url as a string +// joins the BaseUrl path with the given relative path and returns the url as a string func withBaseURL(state *ProgramState, rel string) string { - cpy := state.baseURL.Path - state.baseURL.Path = path.Join(state.baseURL.Path, rel) - res := state.baseURL.String() - state.baseURL.Path = cpy + cpy := state.BaseURL.Path + state.BaseURL.Path = path.Join(state.BaseURL.Path, rel) + res := state.BaseURL.String() + state.BaseURL.Path = cpy return res } @@ -224,7 +228,7 @@ func gen(tmpl *template.Template, data interface{}, out io.Writer) { func copy(src, dest string) { input, err := ioutil.ReadFile(src) if err != nil { - log.Fatalf("Could not open srcDir file for copying: %s\n%s\n", src, err) + log.Fatalf("Could not open SrcDir file for copying: %s\n%s\n", src, err) } err = ioutil.WriteFile(dest, input, 0644) if err != nil { @@ -232,19 +236,33 @@ func copy(src, dest string) { } } -func filter(state *ProgramState, entries []fs.FileInfo) []fs.FileInfo { - filtered := []fs.FileInfo{} +// NOTA: avevo bisogno di una funzione che filtri sia Directory che Files +// Non sono riuscito in breve a creare tale cosa: (dovrebbe avere in input un interfaccia +// che generalizzi il Name per directory e Files, e avere una funzione in input che dica come +// filtrare) +func filterDirs(state *ProgramState, entries []Directory) []Directory { + filtered := []Directory{} for _, entry := range entries { - if entry.IsDir() && !state.excludeRegEx.MatchString(entry.Name()) || - (!entry.IsDir() && state.includeRegEx.MatchString(entry.Name()) && - !state.excludeRegEx.MatchString(entry.Name())) { + if !state.ExcludeRegEx.MatchString(entry.Name) { filtered = append(filtered, entry) } } return filtered } -// SECTION CLARITY: remove this later, just tmp for clarity +// VEDI NOTA filterDirs +func filterFiles(state *ProgramState, entries []File) []File { + filtered := []File{} + for _, entry := range entries { + if state.IncludeRegEx.MatchString(entry.Name) && !state.ExcludeRegEx.MatchString(entry.Name) { + filtered = append(filtered, entry) + } + } + return filtered +} + +// FIXME: i have to sort both Directories and Files, need a way to make +// them both at once // sort by isDirectory and alphabetical naming func sortAlphabetically(files []os.FileInfo) { sort.Slice(files, func(i, j int) bool { @@ -256,127 +274,147 @@ func sortAlphabetically(files []os.FileInfo) { }) } -func generate(state *ProgramState, m *minify.M, dirName string, parts []string) bool { - filesInDir, err := ioutil.ReadDir(dirName) - if err != nil { - log.Fatalf("Could not read input directory: %s\n%s\n", dirName, err) - } - - filteredFiles := filter(state, filesInDir) - if len(filteredFiles) == 0 { - return state.isEmpty - } - - if state.enableSort { - sortAlphabetically(filteredFiles) - } +// REGION GENERATE +// TODO: these functions should be later generalized with interface and so on... +// the function parameters are temporary, i have to find a way to reduce it... - // CHECK IF OUTPUT DIRECTORY HAS GOOD PERMS +// Generate the header and the double dots back anchor when appropriate +func generateHeader(state *ProgramState, parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) - outDir := path.Join(state.destDir, rel) - err = os.Mkdir(outDir, os.ModePerm) - if err != nil { - log.Fatalf("Could not create output *sub*directory: %s\n%s\n", outDir, err) - } - - // LOAD HTML - htmlPath := path.Join(outDir, "index.html") - html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err) - } + p, url := []Dir{}, "" + for _, part := range parts { + url = path.Join(url, part) + p = append(p, Dir{Name: part, URL: withBaseURL(state, url)}) + } + gen(header, Header{ + Root: Dir{ + Name: strings.TrimPrefix(strings.TrimSuffix(state.BaseURL.Path, "/"), "/"), + URL: state.BaseURL.String(), + }, + Parts: p, + FullPath: path.Join(state.BaseURL.Path+rel) + "/", + Stylesheet: template.CSS(style), + }, outBuff) +} - out := new(bytes.Buffer) - // TRACKER: variabili esterne usate: - // parts - // baseURL - // Generate the header and the double dots back anchor when appropriate - { - p, url := []Dir{}, "" - for _, part := range parts { - url = path.Join(url, part) - p = append(p, Dir{Name: part, URL: withBaseURL(state, url)}) - } - gen(header, Header{ - Root: Dir{ - Name: strings.TrimPrefix(strings.TrimSuffix(state.baseURL.Path, "/"), "/"), - URL: state.baseURL.String(), - }, - Parts: p, - FullPath: path.Join(state.baseURL.Path+rel) + "/", - Stylesheet: template.CSS(style), - }, out) - } - - // TRACKER: variabili esterne usate: - // rel - // Populate the back line - { - if len(parts) != 0 { - gen(line, Line{ - IsDir: true, - Name: "..", - URL: withBaseURL(state, path.Join(rel, "..")), - Size: humanize.Bytes(0), - }, out) - } +// populate the back line +func generateBackLine(state *ProgramState, parts []string, outBuff *bytes.Buffer) { + rel := path.Join(parts...) + if len(parts) != 0 { + gen(line, Line{ + IsDir: true, + Name: "..", + URL: withBaseURL(state, path.Join(rel, "..")), + Size: humanize.Bytes(0), + }, outBuff) } +} - // TRACKER: variabili esterne usate: - // filteredFiles - // rel - // dirName - // outDir - // linkSuffix - // line - // state ProgramState - for _, entry := range filteredFiles { - pth := path.Join(dirName, entry.Name()) +func generateDirectories(dirs []Directory, state *ProgramState, parts []string, outBuff *bytes.Buffer) { + rel := path.Join(parts...) + dirName := path.Join(state.SrcDir, rel) + outDir := path.Join(state.DestDir, rel) + for _, dirEntry := range dirs { + dirPath := path.Join(dirName, dirEntry.Name) // Avoid recursive infinite loop - if pth == outDir { + if dirPath == outDir { continue } data := Line{ - IsDir: entry.IsDir(), - Name: entry.Name(), - URL: withBaseURL(state, path.Join(rel, entry.Name())), - Size: humanize.Bytes(uint64(entry.Size())), - Date: entry.ModTime(), + IsDir: true, + Name: dirEntry.Name, + URL: withBaseURL(state, path.Join(rel, dirEntry.Name)), + Size: humanize.Bytes(uint64(dirEntry.Size)), + Date: dirEntry.ModTime, } - if strings.HasSuffix(pth, linkSuffix) { + // FIX: fix empty flag here, i shouldnt generate if dir is empty! + writeHTML(state, &dirEntry, append(parts, dirEntry.Name)) + gen(line, data, outBuff) + } +} + +func generateFiles(files []File, state *ProgramState, parts []string, outBuff *bytes.Buffer) { + rel := path.Join(parts...) + dirName := path.Join(state.SrcDir, rel) + outDir := path.Join(state.DestDir, rel) + for _, fileEntry := range files { + + filePath := path.Join(dirName, fileEntry.Name) + data := Line{ + IsDir: false, + Name: fileEntry.Name, + URL: withBaseURL(state, path.Join(rel, fileEntry.Name)), + Size: humanize.Bytes(uint64(fileEntry.Size)), + Date: fileEntry.ModTime, + } + if strings.HasSuffix(filePath, linkSuffix) { data.Name = data.Name[:len(data.Name)-len(linkSuffix)] // get name without extension data.Size = humanize.Bytes(0) - raw, err := ioutil.ReadFile(pth) + raw, err := ioutil.ReadFile(filePath) if err != nil { - log.Fatalf("Could not read link file: %s\n%s\n", pth, err) + log.Fatalf("Could not read link file: %s\n%s\n", filePath, err) } rawStr := string(raw) u, err := url.Parse(strings.TrimSpace(rawStr)) if err != nil { - log.Fatalf("Could not parse URL in file: %s\nThe value is: %s\n%s\n", pth, raw, err) + log.Fatalf("Could not parse URL in file: %s\nThe value is: %s\n%s\n", filePath, raw, err) } data.URL = u.String() - gen(line, data, out) + gen(line, data, outBuff) continue } + gen(line, data, outBuff) + // Copy all files over to the web root + copy(filePath, path.Join(outDir, fileEntry.Name)) + } +} - // TODO: simplify this logic - // Only list directories when recursing and only those which are not empty - if !entry.IsDir() || state.isRecursive && generate(state, m, pth, append(parts, entry.Name())) { - gen(line, data, out) - } +// END REGION GENERATE - // Copy all files over to the web root - if !entry.IsDir() { - copy(pth, path.Join(outDir, entry.Name())) - } +func writeHTML(state *ProgramState, directory *Directory, parts []string) { + directory.Files = filterFiles(state, directory.Files) + directory.Directories = filterDirs(state, directory.Directories) + + rel := path.Join(parts...) + srcDirName := path.Join(state.SrcDir, rel) + outDir := path.Join(state.DestDir, rel) + log.Printf("Copying from %s\n", srcDirName) + log.Printf("To directory %s\n", outDir) + // FIXME + if len(directory.Directories)+len(directory.Files) == 0 { + return // state.IsEmpty + } + + if state.EnableSort { + // TODO: fix these types!!!! i cant run sort on directory and files! + // sortAlphabetically(directory.Files) } + + // CHECK IF OUTPUT DIRECTORY HAS GOOD PERMS + err := os.Mkdir(outDir, os.ModePerm) + if err != nil { + log.Fatalf("Could not create output *sub*directory: %s\n%s\n", outDir, err) + } + + // CREATE HTMLFILE + htmlPath := path.Join(outDir, "index.html") + html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err) + } + + out := new(bytes.Buffer) + generateHeader(state, parts, out) + generateBackLine(state, parts, out) + generateDirectories(directory.Directories, state, parts, out) + generateFiles(directory.Files, state, parts, out) gen(footer, Footer{Date: time.Now()}, out) - err = m.Minify("text/html", html, out) + + err = state.Minifier.Minify("text/html", html, out) if err != nil { log.Fatalf("Could not write to index.html: %s\n%s\n", htmlPath, err) } @@ -384,9 +422,7 @@ func generate(state *ProgramState, m *minify.M, dirName string, parts []string) if err != nil { log.Fatalf("Could not write to close index.html: %s\n%s\n", htmlPath, err) } - log.Printf("Generated data for directory: %s\n", dirName) - - return !state.isEmpty + log.Printf("Generated data for directory: %s\n", srcDirName) } func loadTemplate(name string, path string, def *string, dest **template.Template) { @@ -409,18 +445,18 @@ func loadTemplate(name string, path string, def *string, dest **template.Templat func logState(state *ProgramState) { log.Println("Running with parameters:") - log.Println("\tInclude:\t", state.includeRegExStr) - log.Println("\tExclude:\t", state.excludeRegExStr) - log.Println("\tRecursive:\t", state.isRecursive) - log.Println("\tEmpty:\t\t", state.isEmpty) - log.Println("\tConvert links:\t", state.convertLink) - log.Println("\tSource:\t\t", state.srcDir) - log.Println("\tDestination:\t", state.destDir) + log.Println("\tInclude:\t", state.IncludeRegExStr) + log.Println("\tExclude:\t", state.ExcludeRegExStr) + log.Println("\tRecursive:\t", state.IsRecursive) + log.Println("\tEmpty:\t\t", state.IsEmpty) + log.Println("\tConvert links:\t", state.ConvertLink) + log.Println("\tSource:\t\t", state.SrcDir) + log.Println("\tDestination:\t", state.DestDir) log.Println("\tBase URL:\t", state.URL) - log.Println("\tStyle:\t\t", state.styleTemplate) - log.Println("\tFooter:\t\t", state.footerTemplate) - log.Println("\tHeader:\t\t", state.headerTemplate) - log.Println("\tline:\t\t", state.lineTemplate) + log.Println("\tStyle:\t\t", state.StyleTemplate) + log.Println("\tFooter:\t\t", state.FooterTemplate) + log.Println("\tHeader:\t\t", state.HeaderTemplate) + log.Println("\tline:\t\t", state.LineTemplate) } func getAbsolutePath(filePath string) string { @@ -449,19 +485,19 @@ func clearDirectory(filePath string) { // handles every input parameter of the Program, returns it in ProgramState. // if something its wrong, the whole program just panick-exits func initProgram(state *ProgramState) { - state.includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") - state.excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") - state.isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") - state.isEmpty = *flag.Bool("empty", false, "Whether to list empty directories") - state.enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") + state.IncludeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") + state.ExcludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") + state.IsRecursive = *flag.Bool("r", true, "Recursively scan the file tree") + state.IsEmpty = *flag.Bool("empty", false, "Whether to list empty directories") + state.EnableSort = *flag.Bool("sort", true, "Sort files A-z and by type") state.URL = *flag.String("b", "http://localhost", "The base URL") - state.convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") - state.styleTemplate = *flag.String("style", "", "Use a custom stylesheet file") - state.footerTemplate = *flag.String("footer", "", "Use a custom footer template") - state.headerTemplate = *flag.String("header", "", "Use a custom header template") - state.lineTemplate = *flag.String("line", "", "Use a custom line template") - state.srcDir = defaultSrc - state.destDir = defaultDest + state.ConvertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") + state.StyleTemplate = *flag.String("style", "", "Use a custom stylesheet file") + state.FooterTemplate = *flag.String("footer", "", "Use a custom footer template") + state.HeaderTemplate = *flag.String("header", "", "Use a custom header template") + state.LineTemplate = *flag.String("line", "", "Use a custom line template") + state.SrcDir = defaultSrc + state.DestDir = defaultDest flag.Parse() args := flag.Args() @@ -469,62 +505,81 @@ func initProgram(state *ProgramState) { log.Fatal("Invalid number of arguments, expected two at max (source and dest)") } if len(args) == 1 { - state.destDir = args[0] + state.DestDir = args[0] } else if len(args) == 2 { - state.srcDir = args[0] - state.destDir = args[1] + state.SrcDir = args[0] + state.DestDir = args[1] } // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose logState(state) - state.srcDir = getAbsolutePath(state.srcDir) - state.destDir = getAbsolutePath(state.destDir) - clearDirectory(state.destDir) + state.SrcDir = getAbsolutePath(state.SrcDir) + state.DestDir = getAbsolutePath(state.DestDir) var err error - state.includeRegEx, err = regexp.Compile(state.includeRegExStr) + state.IncludeRegEx, err = regexp.Compile(state.IncludeRegExStr) if err != nil { log.Fatal("Invalid regexp for include matching", err) } - state.excludeRegEx, err = regexp.Compile(state.excludeRegExStr) + state.ExcludeRegEx, err = regexp.Compile(state.ExcludeRegExStr) if err != nil { log.Fatal("Invalid regexp for exclude matching", err) } - state.baseURL, err = url.Parse(state.URL) + state.BaseURL, err = url.Parse(state.URL) if err != nil { log.Fatalf("Could not parse base URL: %s\n%s\n", state.URL, err) } + state.Minifier = minify.New() + state.Minifier.AddFunc("text/css", css.Minify) + state.Minifier.AddFunc("text/html", html.Minify) + state.Minifier.AddFunc("application/javascript", js.Minify) + // TODO: use the registry design pattern to generalize the template loading, parsing and execution // This section should not belong to initProgram because it doesnt modify things on ProgramState, // just needs access - loadTemplate("header", state.headerTemplate, &rawHeader, &header) - loadTemplate("line", state.lineTemplate, &rawLine, &line) - loadTemplate("footer", state.footerTemplate, &rawFooter, &footer) + loadTemplate("header", state.HeaderTemplate, &rawHeader, &header) + loadTemplate("line", state.LineTemplate, &rawLine, &line) + loadTemplate("footer", state.FooterTemplate, &rawFooter, &footer) - if state.styleTemplate != "" { + if state.StyleTemplate != "" { var content []byte - if content, err = ioutil.ReadFile(state.styleTemplate); err != nil { - log.Fatalf("Could not read stylesheet file %s:\n%s\n", state.styleTemplate, err) + if content, err = ioutil.ReadFile(state.StyleTemplate); err != nil { + log.Fatalf("Could not read stylesheet file %s:\n%s\n", state.StyleTemplate, err) } style = string(content) } } +func prepareDirectories(source, dest string) { + // TODO: add fix for the case where source = "../../" as discussed + + // check inputDir is readable + var err error + _, err = os.OpenFile(source, os.O_RDONLY, 0666) + if err != nil && os.IsPermission(err) { + log.Fatalf("Could not read input directory: %s\n%s\n", source, err) + } + + // check if outputDir is writable + _, err = os.OpenFile(dest, os.O_WRONLY, 0666) + if err != nil && os.IsPermission(err) { + log.Fatalf("Could not write in output directory: %s\n%s\n", dest, err) + } + + clearDirectory(dest) +} + func main() { var state ProgramState var srcStructure Directory initProgram(&state) - - err := GetDirectoryStructure(state.srcDir, state.isRecursive, &srcStructure) + prepareDirectories(state.SrcDir, state.DestDir) + err := GetDirectoryStructure(state.SrcDir, state.IsRecursive, &srcStructure) if err != nil { log.Fatalf("Error when creating the directory structure:\n%s\n", err) } - minifier := minify.New() - minifier.AddFunc("text/css", css.Minify) - minifier.AddFunc("text/html", html.Minify) - minifier.AddFunc("application/javascript", js.Minify) - generate(&state, minifier, state.srcDir, []string{}) + writeHTML(&state, &srcStructure, []string{}) } From 77d23c0bd2d644dac6877e24d2ec89a49dce6f42 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 17 Jun 2022 01:02:24 +0200 Subject: [PATCH 03/16] wip --- go.mod | 2 + go.sum | 10 ++ statik.go | 295 ++++++++++++++++++++++++++---------------------------- 3 files changed, 155 insertions(+), 152 deletions(-) diff --git a/go.mod b/go.mod index a2f1b87..a21b823 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.0 // indirect github.com/tdewolff/minify/v2 v2.11.2 // indirect github.com/tdewolff/parse/v2 v2.5.29 // indirect + golang.org/x/net v0.0.0-20220615171555-694bf12d69de // indirect ) diff --git a/go.sum b/go.sum index 31a3cb2..db3b20f 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fsnotify/fsnotify v1.5.3/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/tdewolff/minify/v2 v2.11.2 h1:PpaPWhNlMVjkAKaOj0bbPv6KCVnrm8jbVwG7OtSdAqw= @@ -10,4 +12,12 @@ github.com/tdewolff/minify/v2 v2.11.2/go.mod h1:NxozhBtgUVypPLzQdV96wkIu9J9vAiVm github.com/tdewolff/parse/v2 v2.5.29 h1:Uf0OtZL9YaUXTuHEOitdo9lD90P0XTwCjZi+KbGChuM= github.com/tdewolff/parse/v2 v2.5.29/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220615171555-694bf12d69de h1:ogOG2+P6LjO2j55AkRScrkB2BFpd+Z8TY2wcM0Z3MGo= +golang.org/x/net v0.0.0-20220615171555-694bf12d69de/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/statik.go b/statik.go index 4f3bcb0..070c161 100644 --- a/statik.go +++ b/statik.go @@ -7,6 +7,7 @@ import ( "flag" "html/template" "io" + "io/fs" "io/ioutil" "log" "net/url" @@ -19,6 +20,7 @@ import ( "time" "github.com/dustin/go-humanize" + "github.com/gabriel-vasile/mimetype" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/css" "github.com/tdewolff/minify/v2/html" @@ -26,31 +28,38 @@ import ( ) // Describes the state of every main variable of the program -type ProgramState struct { - IsRecursive bool - IsEmpty bool - EnableSort bool - ConvertLink bool - - // TODO: i have to convert these template string in other form, so that i can generalize the - // generation - StyleTemplate string - FooterTemplate string - HeaderTemplate string - LineTemplate string - - SrcDir string - DestDir string - - IncludeRegEx *regexp.Regexp - ExcludeRegEx *regexp.Regexp - IncludeRegExStr string - ExcludeRegExStr string - URL string - BaseURL *url.URL - - Minifier *minify.M -} +var ( + //go:embed "style.css" + styleTemplate string + //go:embed "header.gohtml" + headerTemplate string + //go:embed "line.gohtml" + lineTemplate string + //go:embed "footer.gohtml" + footerTemplate string + + header, footer, line *template.Template + minifier *minify.M + + srcDir string + destDir string + + isRecursive bool + isEmpty bool + enableSort bool + convertLink bool + includeRegEx *regexp.Regexp + excludeRegEx *regexp.Regexp + includeRegExStr string + excludeRegExStr string + baseURL *url.URL +) + +const ( + linkSuffix = ".link" + defaultSrc = "./" + defaultDest = "site" +) type Dir struct { Name string @@ -95,128 +104,108 @@ type Directory struct { Name string `json:"name"` Directories []Directory `json:"directories"` Files []File `json:"files"` - Size uint64 `json:"size"` + Size int64 `json:"size"` ModTime time.Time `json:"time"` } type FuzzyFile struct { - Path string `json:"path"` - Name string `json:"name"` - Mime string `json:"mime"` + Name string `json:"name"` + Path string `json:"path"` + SourcePath string `json:"-"` + URL *url.URL `json:"url"` + Mime string `json:"mime"` } type File struct { FuzzyFile - Size uint64 `json:"size"` + Size int64 `json:"size"` ModTime time.Time `json:"time"` } -const linkSuffix = ".link" -const defaultSrc = "./" -const defaultDest = "site" - -// WARNING: don't call this with directory FileInfo, not supported -func getFile(file os.FileInfo, path string) File { - return File{ - FuzzyFile: FuzzyFile{ - Path: path, - Name: file.Name(), - Mime: "tmp", // TODO: make a function that returns the correct mime - }, - Size: uint64(file.Size()), - ModTime: file.ModTime(), +// joins the baseURL with the given relative path in a new URL instance +func withBaseURL(rel string) (url *url.URL, err error) { + url, err = url.Parse(baseURL.String()) + if err != nil { + return } + url.Path = path.Join(baseURL.Path, rel) + return } -// WARNING: don't call this with FileInfo that is not a directory, not supported -func getDirectory(file os.FileInfo, path string) Directory { - return Directory{ - Path: path, - Name: file.Name(), - Directories: []Directory{}, - Files: []File{}, - Size: uint64(file.Size()), - ModTime: file.ModTime(), +func newFile(info os.FileInfo, dir string) (f File, err error) { + if info.IsDir() { + return File{}, errors.New("newFile has been called with a os.FileInfo if type Directory") } -} -// Separates files and directories -func unpackFiles(fileInfo []os.FileInfo) ([]os.FileInfo, []os.FileInfo) { - var files []os.FileInfo - var dirs []os.FileInfo - for _, file := range fileInfo { - if !file.IsDir() { - files = append(files, file) - } else { - dirs = append(dirs, file) - } + var ( + rel string + url *url.URL + mime *mimetype.MIME + ) + abs := path.Join(dir, info.Name()) + if rel, err = filepath.Rel(srcDir, abs); err != nil { + return + } + if url, err = withBaseURL(rel); err != nil { + return + } + if mime, err = mimetype.DetectFile(abs); err != nil { + return } - return files, dirs + + return File{ + FuzzyFile: FuzzyFile{ + Name: info.Name(), + Path: rel, + URL: url, + Mime: mime.String(), + }, + Size: info.Size(), + ModTime: info.ModTime(), + }, nil } -func IsPathValid(path string) error { +func isDir(path string) (err error) { dir, err := os.Stat(path) if err != nil { return err } if !dir.IsDir() { - return errors.New("the given path does not correspond to a directory") + return errors.New("Expected a directory") } return nil } -func GetDirectoryStructure(path string, recursive bool, directory *Directory) error { - err := IsPathValid(path) - if err != nil { - return err - } - - filesInDir, err := ioutil.ReadDir(path) - if err != nil { - return err - } - - files, dirs := unpackFiles(filesInDir) - for _, file := range files { - directory.Files = append(directory.Files, getFile(file, path)) - } +func (d Directory) isEmpty() bool { + return len(d.Directories) == 0 && len(d.Files) == 0 +} - for _, dir := range dirs { - directory.Directories = append(directory.Directories, getDirectory(dir, path)) +func GetDirectoryStructure(base string) (dir Directory, err error) { + var ( + infos []fs.FileInfo + subdir Directory + file File + ) + if infos, err = ioutil.ReadDir(base); err != nil { + return } - if recursive { - for idx, dir := range directory.Directories { - dirName := filepath.Join(path, dir.Name) - err := GetDirectoryStructure(dirName, true, &directory.Directories[idx]) - if err != nil { - return err + for _, info := range infos { + if info.IsDir() && isRecursive { + if subdir, err = GetDirectoryStructure(path.Join(base, info.Name())); err != nil { + return + } + if !subdir.isEmpty() { + dir.Directories = append(dir.Directories, subdir) + } + } else { + if file, err = newFile(info, base); err != nil { + return } + dir.Files = append(dir.Files, file) } } - return nil -} - -var ( - //go:embed "style.css" - style string - //go:embed "header.gohtml" - rawHeader string - //go:embed "line.gohtml" - rawLine string - //go:embed "footer.gohtml" - rawFooter string - - header, footer, line *template.Template -) - -// joins the BaseUrl path with the given relative path and returns the url as a string -func withBaseURL(state *ProgramState, rel string) string { - cpy := state.BaseURL.Path - state.BaseURL.Path = path.Join(state.BaseURL.Path, rel) - res := state.BaseURL.String() - state.BaseURL.Path = cpy - return res + return } func gen(tmpl *template.Template, data interface{}, out io.Writer) { @@ -485,19 +474,42 @@ func clearDirectory(filePath string) { // handles every input parameter of the Program, returns it in ProgramState. // if something its wrong, the whole program just panick-exits func initProgram(state *ProgramState) { - state.IncludeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") - state.ExcludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") - state.IsRecursive = *flag.Bool("r", true, "Recursively scan the file tree") - state.IsEmpty = *flag.Bool("empty", false, "Whether to list empty directories") - state.EnableSort = *flag.Bool("sort", true, "Sort files A-z and by type") - state.URL = *flag.String("b", "http://localhost", "The base URL") - state.ConvertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") - state.StyleTemplate = *flag.String("style", "", "Use a custom stylesheet file") - state.FooterTemplate = *flag.String("footer", "", "Use a custom footer template") - state.HeaderTemplate = *flag.String("header", "", "Use a custom header template") - state.LineTemplate = *flag.String("line", "", "Use a custom line template") - state.SrcDir = defaultSrc - state.DestDir = defaultDest +} + +func prepareDirectories(source, dest string) { + // TODO: add fix for the case where source = "../../" as discussed + + // check inputDir is readable + var err error + _, err = os.OpenFile(source, os.O_RDONLY, 0666) + if err != nil && os.IsPermission(err) { + log.Fatalf("Could not read input directory: %s\n%s\n", source, err) + } + + // check if outputDir is writable + _, err = os.OpenFile(dest, os.O_WRONLY, 0666) + if err != nil && os.IsPermission(err) { + log.Fatalf("Could not write in output directory: %s\n%s\n", dest, err) + } + + clearDirectory(dest) +} + +func main() { + var srcStructure Directory + includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") + excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") + isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") + isEmpty = *flag.Bool("empty", false, "Whether to list empty directories") + enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") + rawURL := *flag.String("b", "http://localhost", "The base URL") + convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") + styleTemplate = *flag.String("style", "", "Use a custom stylesheet file") + footerTemplate = *flag.String("footer", "", "Use a custom footer template") + headerTemplate = *flag.String("header", "", "Use a custom header template") + lineTemplate = *flag.String("line", "", "Use a custom line template") + srcDir = defaultSrc + destDir = defaultDest flag.Parse() args := flag.Args() @@ -526,7 +538,7 @@ func initProgram(state *ProgramState) { log.Fatal("Invalid regexp for exclude matching", err) } - state.BaseURL, err = url.Parse(state.URL) + URL, err = url.Parse(rawURL) if err != nil { log.Fatalf("Could not parse base URL: %s\n%s\n", state.URL, err) } @@ -550,33 +562,12 @@ func initProgram(state *ProgramState) { } style = string(content) } -} - -func prepareDirectories(source, dest string) { - // TODO: add fix for the case where source = "../../" as discussed - - // check inputDir is readable - var err error - _, err = os.OpenFile(source, os.O_RDONLY, 0666) - if err != nil && os.IsPermission(err) { - log.Fatalf("Could not read input directory: %s\n%s\n", source, err) - } - - // check if outputDir is writable - _, err = os.OpenFile(dest, os.O_WRONLY, 0666) - if err != nil && os.IsPermission(err) { - log.Fatalf("Could not write in output directory: %s\n%s\n", dest, err) + err := isDir(state.SrcDir) + if err != nil { + return err } - - clearDirectory(dest) -} - -func main() { - var state ProgramState - var srcStructure Directory - initProgram(&state) prepareDirectories(state.SrcDir, state.DestDir) - err := GetDirectoryStructure(state.SrcDir, state.IsRecursive, &srcStructure) + dir, err := GetDirectoryStructure(state.SrcDir, state.IsRecursive, &srcStructure) if err != nil { log.Fatalf("Error when creating the directory structure:\n%s\n", err) } From 99b8b18a26b962664be2a07ac80f51edfbe06d32 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 17 Jun 2022 11:54:12 +0200 Subject: [PATCH 04/16] wip cleanup --- go.mod | 2 +- statik.go | 288 ++++++++++++++++++++++++++---------------------------- 2 files changed, 138 insertions(+), 152 deletions(-) diff --git a/go.mod b/go.mod index a21b823..cb0382c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/lucat1/statik -go 1.17 +go 1.18 require ( github.com/dustin/go-humanize v1.0.0 // indirect diff --git a/statik.go b/statik.go index 070c161..194dcb9 100644 --- a/statik.go +++ b/statik.go @@ -5,6 +5,7 @@ import ( _ "embed" "errors" "flag" + "fmt" "html/template" "io" "io/fs" @@ -29,8 +30,6 @@ import ( // Describes the state of every main variable of the program var ( - //go:embed "style.css" - styleTemplate string //go:embed "header.gohtml" headerTemplate string //go:embed "line.gohtml" @@ -38,9 +37,12 @@ var ( //go:embed "footer.gohtml" footerTemplate string + //go:embed "style.css" + style string header, footer, line *template.Template minifier *minify.M + workDir string srcDir string destDir string @@ -99,9 +101,13 @@ type Template interface { Tmpl() *template.Template // just return the template pointer } +type Named interface { + GetName() string +} + type Directory struct { - Path string `json:"path"` Name string `json:"name"` + Path string `json:"path"` Directories []Directory `json:"directories"` Files []File `json:"files"` Size int64 `json:"size"` @@ -122,6 +128,9 @@ type File struct { ModTime time.Time `json:"time"` } +func (d Directory) GetName() string { return d.Name } +func (f File) GetName() string { return f.FuzzyFile.Name } + // joins the baseURL with the given relative path in a new URL instance func withBaseURL(rel string) (url *url.URL, err error) { url, err = url.Parse(baseURL.String()) @@ -134,7 +143,7 @@ func withBaseURL(rel string) (url *url.URL, err error) { func newFile(info os.FileInfo, dir string) (f File, err error) { if info.IsDir() { - return File{}, errors.New("newFile has been called with a os.FileInfo if type Directory") + return File{}, errors.New("newFile has been called with a os.FileInfo of type Directory") } var ( @@ -171,7 +180,7 @@ func isDir(path string) (err error) { return err } if !dir.IsDir() { - return errors.New("Expected a directory") + return fmt.Errorf("Expected %s to be a directory", path) } return nil } @@ -180,31 +189,54 @@ func (d Directory) isEmpty() bool { return len(d.Directories) == 0 && len(d.Files) == 0 } -func GetDirectoryStructure(base string) (dir Directory, err error) { +func sortAlpha[T Named](infos []T) { + sort.Slice(infos, func(i, j int) bool { + return infos[i].GetName() < infos[j].GetName() + }) +} + +func walk(base string) (dir Directory, err error) { var ( infos []fs.FileInfo subdir Directory file File + rel string ) if infos, err = ioutil.ReadDir(base); err != nil { + return dir, fmt.Errorf("Could not read directory %s:\n%s", base, err) + } + + if infos[0], err = os.Stat(base); err != nil { + return dir, fmt.Errorf("Could not stat directory %s:\n%s", base, err) + } + if rel, err = filepath.Rel(srcDir, base); err != nil { return } + dir = Directory{ + Name: infos[0].Name(), + Path: rel, + Size: infos[0].Size(), + ModTime: infos[0].ModTime(), + } + for _, info := range infos { - if info.IsDir() && isRecursive { - if subdir, err = GetDirectoryStructure(path.Join(base, info.Name())); err != nil { + if info.IsDir() && isRecursive && includeDir(info) { + if subdir, err = walk(path.Join(base, info.Name())); err != nil { return } if !subdir.isEmpty() { dir.Directories = append(dir.Directories, subdir) } - } else { + } else if !info.IsDir() && includeFile(info) { if file, err = newFile(info, base); err != nil { - return + return dir, fmt.Errorf("Error while generating the File structure:\n%s", err) } dir.Files = append(dir.Files, file) } } + sortAlpha(dir.Files) + sortAlpha(dir.Directories) return } @@ -225,42 +257,12 @@ func copy(src, dest string) { } } -// NOTA: avevo bisogno di una funzione che filtri sia Directory che Files -// Non sono riuscito in breve a creare tale cosa: (dovrebbe avere in input un interfaccia -// che generalizzi il Name per directory e Files, e avere una funzione in input che dica come -// filtrare) -func filterDirs(state *ProgramState, entries []Directory) []Directory { - filtered := []Directory{} - for _, entry := range entries { - if !state.ExcludeRegEx.MatchString(entry.Name) { - filtered = append(filtered, entry) - } - } - return filtered -} - -// VEDI NOTA filterDirs -func filterFiles(state *ProgramState, entries []File) []File { - filtered := []File{} - for _, entry := range entries { - if state.IncludeRegEx.MatchString(entry.Name) && !state.ExcludeRegEx.MatchString(entry.Name) { - filtered = append(filtered, entry) - } - } - return filtered +func includeDir(info fs.FileInfo) bool { + return !excludeRegEx.MatchString(info.Name()) } -// FIXME: i have to sort both Directories and Files, need a way to make -// them both at once -// sort by isDirectory and alphabetical naming -func sortAlphabetically(files []os.FileInfo) { - sort.Slice(files, func(i, j int) bool { - isFirstEntryDir := files[i].IsDir() - isSecondEntryDir := files[j].IsDir() - return isFirstEntryDir && !isSecondEntryDir || - (isFirstEntryDir || !isSecondEntryDir) && - files[i].Name() < files[j].Name() - }) +func includeFile(info fs.FileInfo) bool { + return includeRegEx.MatchString(info.Name()) && !excludeRegEx.MatchString(info.Name()) } // REGION GENERATE @@ -268,7 +270,7 @@ func sortAlphabetically(files []os.FileInfo) { // the function parameters are temporary, i have to find a way to reduce it... // Generate the header and the double dots back anchor when appropriate -func generateHeader(state *ProgramState, parts []string, outBuff *bytes.Buffer) { +func generateHeader(parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) p, url := []Dir{}, "" for _, part := range parts { @@ -287,7 +289,7 @@ func generateHeader(state *ProgramState, parts []string, outBuff *bytes.Buffer) } // populate the back line -func generateBackLine(state *ProgramState, parts []string, outBuff *bytes.Buffer) { +func generateBackLine(parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) if len(parts) != 0 { gen(line, Line{ @@ -324,7 +326,7 @@ func generateDirectories(dirs []Directory, state *ProgramState, parts []string, } } -func generateFiles(files []File, state *ProgramState, parts []string, outBuff *bytes.Buffer) { +func generateFiles(files []File, parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) dirName := path.Join(state.SrcDir, rel) outDir := path.Join(state.DestDir, rel) @@ -364,10 +366,7 @@ func generateFiles(files []File, state *ProgramState, parts []string, outBuff *b // END REGION GENERATE -func writeHTML(state *ProgramState, directory *Directory, parts []string) { - directory.Files = filterFiles(state, directory.Files) - directory.Directories = filterDirs(state, directory.Directories) - +func writeHTML(directory *Directory, parts []string) { rel := path.Join(parts...) srcDirName := path.Join(state.SrcDir, rel) outDir := path.Join(state.DestDir, rel) @@ -414,89 +413,78 @@ func writeHTML(state *ProgramState, directory *Directory, parts []string) { log.Printf("Generated data for directory: %s\n", srcDirName) } -func loadTemplate(name string, path string, def *string, dest **template.Template) { - var ( - content []byte - err error - ) +func readIfNotEmpty(path string, dest *string) (err error) { + var content []byte if path != "" { content, err = ioutil.ReadFile(path) if err != nil { - log.Fatalf("Could not read %s template file %s:\n%s\n", name, path, err) + return fmt.Errorf("Could not read file: %s\n%s", path, err) } - *def = string(content) } - *dest, err = template.New(name).Parse(*def) - if err != nil { - log.Fatalf("Could not parse %s template %s:\n%s\n", name, path, err) + *dest = string(content) + return nil +} + +func loadTemplate(name string, path string, buf *string) (tmpl *template.Template, err error) { + if err = readIfNotEmpty(path, buf); err != nil { + return } + if tmpl, err = template.New(name).Parse(*buf); err != nil { + return + } + return } -func logState(state *ProgramState) { +func logState() { log.Println("Running with parameters:") - log.Println("\tInclude:\t", state.IncludeRegExStr) - log.Println("\tExclude:\t", state.ExcludeRegExStr) - log.Println("\tRecursive:\t", state.IsRecursive) - log.Println("\tEmpty:\t\t", state.IsEmpty) - log.Println("\tConvert links:\t", state.ConvertLink) - log.Println("\tSource:\t\t", state.SrcDir) - log.Println("\tDestination:\t", state.DestDir) - log.Println("\tBase URL:\t", state.URL) - log.Println("\tStyle:\t\t", state.StyleTemplate) - log.Println("\tFooter:\t\t", state.FooterTemplate) - log.Println("\tHeader:\t\t", state.HeaderTemplate) - log.Println("\tline:\t\t", state.LineTemplate) + log.Println("\tInclude:\t", includeRegExStr) + log.Println("\tExclude:\t", excludeRegExStr) + log.Println("\tRecursive:\t", isRecursive) + log.Println("\tEmpty:\t\t", isEmpty) + log.Println("\tConvert links:\t", convertLink) + log.Println("\tSource:\t\t", srcDir) + log.Println("\tDestination:\t", destDir) + log.Println("\tBase URL:\t", baseURL) + log.Println("\tStyle:\t\t", styleTemplate) + log.Println("\tFooter:\t\t", footerTemplate) + log.Println("\tHeader:\t\t", headerTemplate) + log.Println("\tline:\t\t", lineTemplate) } -func getAbsolutePath(filePath string) string { - if !filepath.IsAbs(filePath) { - wd, err := os.Getwd() - if err != nil { - log.Fatal("Could not get currently working directory", err) - } - return path.Join(wd, filePath) - } else { - return filePath +func abs(rel string) string { + if filepath.IsAbs(rel) { + return rel } -} -// remove all files from input directory -func clearDirectory(filePath string) { - _, err := os.Stat(filePath) - if err == nil { - err = os.RemoveAll(filePath) - if err != nil { - log.Fatalf("Could not remove output directory previous contents: %s\n%s\n", filePath, err) - } - } + return path.Join(workDir, rel) } -// handles every input parameter of the Program, returns it in ProgramState. -// if something its wrong, the whole program just panick-exits -func initProgram(state *ProgramState) { -} +func sanitizeDirectories() (err error) { + if strings.HasPrefix(srcDir, destDir) { + return errors.New("The output directory cannot be a parent of the input directory") + } -func prepareDirectories(source, dest string) { - // TODO: add fix for the case where source = "../../" as discussed + if _, err = os.OpenFile(srcDir, os.O_RDONLY, 0666); err != nil && os.IsPermission(err) { + return fmt.Errorf("Cannot open source directory for reading: %s\n%s", srcDir, err) + } - // check inputDir is readable - var err error - _, err = os.OpenFile(source, os.O_RDONLY, 0666) - if err != nil && os.IsPermission(err) { - log.Fatalf("Could not read input directory: %s\n%s\n", source, err) + if err := isDir(srcDir); err != nil { + return err } // check if outputDir is writable - _, err = os.OpenFile(dest, os.O_WRONLY, 0666) - if err != nil && os.IsPermission(err) { - log.Fatalf("Could not write in output directory: %s\n%s\n", dest, err) + if _, err = os.OpenFile(destDir, os.O_WRONLY, 0666); err != nil && os.IsPermission(err) { + return fmt.Errorf("Cannot open output directory for writing: %s\n%s", destDir, err) } - clearDirectory(dest) + if err = os.RemoveAll(destDir); err != nil { + return fmt.Errorf("Cannot clear output directory: %s\n%s", destDir, err) + } + return nil } func main() { - var srcStructure Directory + var err error includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") @@ -504,73 +492,71 @@ func main() { enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") rawURL := *flag.String("b", "http://localhost", "The base URL") convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") - styleTemplate = *flag.String("style", "", "Use a custom stylesheet file") - footerTemplate = *flag.String("footer", "", "Use a custom footer template") - headerTemplate = *flag.String("header", "", "Use a custom header template") - lineTemplate = *flag.String("line", "", "Use a custom line template") + styleTemplatePath := *flag.String("style", "", "Use a custom stylesheet file") + headerTemplatePath := *flag.String("header", "", "Use a custom header template") + lineTemplatePath := *flag.String("line", "", "Use a custom line template") + footerTemplatePath := *flag.String("footer", "", "Use a custom footer template") srcDir = defaultSrc destDir = defaultDest flag.Parse() args := flag.Args() if len(args) > 2 { - log.Fatal("Invalid number of arguments, expected two at max (source and dest)") + fmt.Printf("Usage: %s [dest] or [src] [dest]\n", os.Args[0]) + os.Exit(1) } if len(args) == 1 { - state.DestDir = args[0] + destDir = args[0] } else if len(args) == 2 { - state.SrcDir = args[0] - state.DestDir = args[1] + srcDir = args[0] + destDir = args[1] + } + + if workDir, err = os.Getwd(); err != nil { + log.Fatal("Could not get working directory", err) } // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose - logState(state) - state.SrcDir = getAbsolutePath(state.SrcDir) - state.DestDir = getAbsolutePath(state.DestDir) + logState() + srcDir = abs(srcDir) + destDir = abs(destDir) + if err = sanitizeDirectories(); err != nil { + log.Fatal("Error while checking src and dest paths", err) + } - var err error - state.IncludeRegEx, err = regexp.Compile(state.IncludeRegExStr) - if err != nil { + if includeRegEx, err = regexp.Compile(includeRegExStr); err != nil { log.Fatal("Invalid regexp for include matching", err) } - state.ExcludeRegEx, err = regexp.Compile(state.ExcludeRegExStr) - if err != nil { + if excludeRegEx, err = regexp.Compile(excludeRegExStr); err != nil { log.Fatal("Invalid regexp for exclude matching", err) } - URL, err = url.Parse(rawURL) - if err != nil { - log.Fatalf("Could not parse base URL: %s\n%s\n", state.URL, err) + if baseURL, err = url.Parse(rawURL); err != nil { + log.Fatal("Could not parse base URL", err) } - state.Minifier = minify.New() - state.Minifier.AddFunc("text/css", css.Minify) - state.Minifier.AddFunc("text/html", html.Minify) - state.Minifier.AddFunc("application/javascript", js.Minify) + minifier = minify.New() + minifier.AddFunc("text/css", css.Minify) + minifier.AddFunc("text/html", html.Minify) + minifier.AddFunc("application/javascript", js.Minify) - // TODO: use the registry design pattern to generalize the template loading, parsing and execution - // This section should not belong to initProgram because it doesnt modify things on ProgramState, - // just needs access - loadTemplate("header", state.HeaderTemplate, &rawHeader, &header) - loadTemplate("line", state.LineTemplate, &rawLine, &line) - loadTemplate("footer", state.FooterTemplate, &rawFooter, &footer) - - if state.StyleTemplate != "" { - var content []byte - if content, err = ioutil.ReadFile(state.StyleTemplate); err != nil { - log.Fatalf("Could not read stylesheet file %s:\n%s\n", state.StyleTemplate, err) - } - style = string(content) + if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { + log.Fatal("Could not parse header template", err) } - err := isDir(state.SrcDir) - if err != nil { - return err + if line, err = loadTemplate("line", lineTemplatePath, &lineTemplate); err != nil { + log.Fatal("Could not parse line template", err) } - prepareDirectories(state.SrcDir, state.DestDir) - dir, err := GetDirectoryStructure(state.SrcDir, state.IsRecursive, &srcStructure) + if footer, err = loadTemplate("footer", footerTemplatePath, &footerTemplate); err != nil { + log.Fatal("Could not parse footer template", err) + } + if err = readIfNotEmpty(styleTemplatePath, &style); err != nil { + log.Fatal("Could not read stylesheet file", err) + } + + dir, err := walk(srcDir, isRecursive) if err != nil { log.Fatalf("Error when creating the directory structure:\n%s\n", err) } - writeHTML(&state, &srcStructure, []string{}) + writeHTML(dir, []string{}) } From 400b2c031784d98ef321e5dc0712afbff58c25a6 Mon Sep 17 00:00:00 2001 From: Angelo 'Flecart' Huang Date: Fri, 17 Jun 2022 15:08:45 +0200 Subject: [PATCH 05/16] refactor: create buildable srccode create a version that doesn't use state anymore, using the global variables. There is a bug about the templates --- go.sum | 5 ++ statik.go | 178 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 104 insertions(+), 79 deletions(-) diff --git a/go.sum b/go.sum index db3b20f..6f0c792 100644 --- a/go.sum +++ b/go.sum @@ -17,7 +17,12 @@ golang.org/x/net v0.0.0-20220615171555-694bf12d69de h1:ogOG2+P6LjO2j55AkRScrkB2B golang.org/x/net v0.0.0-20220615171555-694bf12d69de/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/statik.go b/statik.go index 194dcb9..e2d21cf 100644 --- a/statik.go +++ b/statik.go @@ -31,17 +31,21 @@ import ( // Describes the state of every main variable of the program var ( //go:embed "header.gohtml" - headerTemplate string + headerTemplate string //go:embed "line.gohtml" - lineTemplate string + lineTemplate string //go:embed "footer.gohtml" - footerTemplate string - + footerTemplate string //go:embed "style.css" style string header, footer, line *template.Template minifier *minify.M + styleTemplatePath string + headerTemplatePath string + lineTemplatePath string + footerTemplatePath string + workDir string srcDir string destDir string @@ -55,6 +59,7 @@ var ( includeRegExStr string excludeRegExStr string baseURL *url.URL + rawURL string ) const ( @@ -108,6 +113,7 @@ type Named interface { type Directory struct { Name string `json:"name"` Path string `json:"path"` + SourcePath string `json:"-"` Directories []Directory `json:"directories"` Files []File `json:"files"` Size int64 `json:"size"` @@ -132,11 +138,8 @@ func (d Directory) GetName() string { return d.Name } func (f File) GetName() string { return f.FuzzyFile.Name } // joins the baseURL with the given relative path in a new URL instance -func withBaseURL(rel string) (url *url.URL, err error) { - url, err = url.Parse(baseURL.String()) - if err != nil { - return - } +func withBaseURL(rel string) (url *url.URL) { + url, _ = url.Parse(baseURL.String()) // its clear that there can't be error here :) url.Path = path.Join(baseURL.Path, rel) return } @@ -155,9 +158,7 @@ func newFile(info os.FileInfo, dir string) (f File, err error) { if rel, err = filepath.Rel(srcDir, abs); err != nil { return } - if url, err = withBaseURL(rel); err != nil { - return - } + url = withBaseURL(rel) if mime, err = mimetype.DetectFile(abs); err != nil { return } @@ -166,6 +167,7 @@ func newFile(info os.FileInfo, dir string) (f File, err error) { FuzzyFile: FuzzyFile{ Name: info.Name(), Path: rel, + SourcePath: dir, URL: url, Mime: mime.String(), }, @@ -197,35 +199,38 @@ func sortAlpha[T Named](infos []T) { func walk(base string) (dir Directory, err error) { var ( - infos []fs.FileInfo - subdir Directory - file File - rel string + infos []fs.FileInfo + sourceInfo fs.FileInfo + subdir Directory + file File + rel string ) if infos, err = ioutil.ReadDir(base); err != nil { return dir, fmt.Errorf("Could not read directory %s:\n%s", base, err) } - if infos[0], err = os.Stat(base); err != nil { + if sourceInfo, err = os.Stat(base); err != nil { return dir, fmt.Errorf("Could not stat directory %s:\n%s", base, err) } + if rel, err = filepath.Rel(srcDir, base); err != nil { - return + return dir, err } dir = Directory{ - Name: infos[0].Name(), - Path: rel, - Size: infos[0].Size(), - ModTime: infos[0].ModTime(), + Name: sourceInfo.Name(), + SourcePath: base, + Path: rel, + Size: sourceInfo.Size(), + ModTime: sourceInfo.ModTime(), } for _, info := range infos { if info.IsDir() && isRecursive && includeDir(info) { if subdir, err = walk(path.Join(base, info.Name())); err != nil { - return + return dir, err } - if !subdir.isEmpty() { + if !subdir.isEmpty() || isEmpty { // include emptydir if isEmptyflag is setted dir.Directories = append(dir.Directories, subdir) } } else if !info.IsDir() && includeFile(info) { @@ -275,15 +280,15 @@ func generateHeader(parts []string, outBuff *bytes.Buffer) { p, url := []Dir{}, "" for _, part := range parts { url = path.Join(url, part) - p = append(p, Dir{Name: part, URL: withBaseURL(state, url)}) + p = append(p, Dir{Name: part, URL: withBaseURL(url).Path}) } gen(header, Header{ Root: Dir{ - Name: strings.TrimPrefix(strings.TrimSuffix(state.BaseURL.Path, "/"), "/"), - URL: state.BaseURL.String(), + Name: strings.TrimPrefix(strings.TrimSuffix(baseURL.Path, "/"), "/"), + URL: baseURL.String(), }, Parts: p, - FullPath: path.Join(state.BaseURL.Path+rel) + "/", + FullPath: path.Join(baseURL.Path+rel) + "/", Stylesheet: template.CSS(style), }, outBuff) } @@ -295,16 +300,16 @@ func generateBackLine(parts []string, outBuff *bytes.Buffer) { gen(line, Line{ IsDir: true, Name: "..", - URL: withBaseURL(state, path.Join(rel, "..")), + URL: withBaseURL(path.Join(rel, "..")).Path, Size: humanize.Bytes(0), }, outBuff) } } -func generateDirectories(dirs []Directory, state *ProgramState, parts []string, outBuff *bytes.Buffer) { +func generateDirectories(dirs []Directory, parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) - dirName := path.Join(state.SrcDir, rel) - outDir := path.Join(state.DestDir, rel) + dirName := path.Join(srcDir, rel) + outDir := path.Join(destDir, rel) for _, dirEntry := range dirs { dirPath := path.Join(dirName, dirEntry.Name) // Avoid recursive infinite loop @@ -315,28 +320,29 @@ func generateDirectories(dirs []Directory, state *ProgramState, parts []string, data := Line{ IsDir: true, Name: dirEntry.Name, - URL: withBaseURL(state, path.Join(rel, dirEntry.Name)), + URL: withBaseURL(path.Join(rel, dirEntry.Name)).Path, Size: humanize.Bytes(uint64(dirEntry.Size)), Date: dirEntry.ModTime, } - // FIX: fix empty flag here, i shouldnt generate if dir is empty! - writeHTML(state, &dirEntry, append(parts, dirEntry.Name)) + writeHTML(&dirEntry, append(parts, dirEntry.Name)) gen(line, data, outBuff) } } func generateFiles(files []File, parts []string, outBuff *bytes.Buffer) { rel := path.Join(parts...) - dirName := path.Join(state.SrcDir, rel) - outDir := path.Join(state.DestDir, rel) + dirName := path.Join(srcDir, rel) + outDir := path.Join(destDir, rel) for _, fileEntry := range files { + // DEBUG REMOVE ME AFTER FIX + fmt.Printf("file probably generated: %s \n", fileEntry.Name) filePath := path.Join(dirName, fileEntry.Name) data := Line{ IsDir: false, Name: fileEntry.Name, - URL: withBaseURL(state, path.Join(rel, fileEntry.Name)), + URL: withBaseURL(path.Join(rel, fileEntry.Name)).Path, Size: humanize.Bytes(uint64(fileEntry.Size)), Date: fileEntry.ModTime, } @@ -356,11 +362,11 @@ func generateFiles(files []File, parts []string, outBuff *bytes.Buffer) { data.URL = u.String() gen(line, data, outBuff) - continue + } else { + // Copy all files over to the web root + copy(filePath, path.Join(outDir, fileEntry.Name)) } gen(line, data, outBuff) - // Copy all files over to the web root - copy(filePath, path.Join(outDir, fileEntry.Name)) } } @@ -368,19 +374,8 @@ func generateFiles(files []File, parts []string, outBuff *bytes.Buffer) { func writeHTML(directory *Directory, parts []string) { rel := path.Join(parts...) - srcDirName := path.Join(state.SrcDir, rel) - outDir := path.Join(state.DestDir, rel) - log.Printf("Copying from %s\n", srcDirName) - log.Printf("To directory %s\n", outDir) - // FIXME - if len(directory.Directories)+len(directory.Files) == 0 { - return // state.IsEmpty - } - - if state.EnableSort { - // TODO: fix these types!!!! i cant run sort on directory and files! - // sortAlphabetically(directory.Files) - } + srcDirName := path.Join(srcDir, rel) + outDir := path.Join(destDir, rel) // CHECK IF OUTPUT DIRECTORY HAS GOOD PERMS err := os.Mkdir(outDir, os.ModePerm) @@ -396,13 +391,13 @@ func writeHTML(directory *Directory, parts []string) { } out := new(bytes.Buffer) - generateHeader(state, parts, out) - generateBackLine(state, parts, out) - generateDirectories(directory.Directories, state, parts, out) - generateFiles(directory.Files, state, parts, out) + generateHeader(parts, out) + generateBackLine(parts, out) + generateDirectories(directory.Directories, parts, out) + generateFiles(directory.Files, parts, out) gen(footer, Footer{Date: time.Now()}, out) - err = state.Minifier.Minify("text/html", html, out) + err = minifier.Minify("text/html", html, out) if err != nil { log.Fatalf("Could not write to index.html: %s\n%s\n", htmlPath, err) } @@ -432,9 +427,29 @@ func loadTemplate(name string, path string, buf *string) (tmpl *template.Templat if tmpl, err = template.New(name).Parse(*buf); err != nil { return } - return + return } +// TODO: debug: remove me after fix! +func loadTemplateOld(name string, path string, def *string, dest **template.Template) { + var ( + content []byte + err error + ) + if path != "" { + content, err = ioutil.ReadFile(path) + if err != nil { + log.Fatalf("Could not read %s template file %s:\n%s\n", name, path, err) + } + *def = string(content) + } + *dest, err = template.New(name).Parse(*def) + if err != nil { + log.Fatalf("Could not parse %s template %s:\n%s\n", name, path, err) + } +} + + func logState() { log.Println("Running with parameters:") log.Println("\tInclude:\t", includeRegExStr) @@ -444,14 +459,14 @@ func logState() { log.Println("\tConvert links:\t", convertLink) log.Println("\tSource:\t\t", srcDir) log.Println("\tDestination:\t", destDir) - log.Println("\tBase URL:\t", baseURL) - log.Println("\tStyle:\t\t", styleTemplate) - log.Println("\tFooter:\t\t", footerTemplate) - log.Println("\tHeader:\t\t", headerTemplate) - log.Println("\tline:\t\t", lineTemplate) + log.Println("\tBase URL:\t", rawURL) + log.Println("\tStyle:\t\t", styleTemplatePath) + log.Println("\tFooter:\t\t", footerTemplatePath) + log.Println("\tHeader:\t\t", headerTemplatePath) + log.Println("\tline:\t\t", lineTemplatePath) } -func abs(rel string) string { +func getAbsPath(rel string) string { if filepath.IsAbs(rel) { return rel } @@ -484,18 +499,19 @@ func sanitizeDirectories() (err error) { } func main() { + // REGION INITGLOBALS var err error includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") isEmpty = *flag.Bool("empty", false, "Whether to list empty directories") enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") - rawURL := *flag.String("b", "http://localhost", "The base URL") + rawURL = *flag.String("b", "http://localhost", "The base URL") convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") - styleTemplatePath := *flag.String("style", "", "Use a custom stylesheet file") - headerTemplatePath := *flag.String("header", "", "Use a custom header template") - lineTemplatePath := *flag.String("line", "", "Use a custom line template") - footerTemplatePath := *flag.String("footer", "", "Use a custom footer template") + styleTemplatePath = *flag.String("style", "", "Use a custom stylesheet file") + headerTemplatePath = *flag.String("header", "", "Use a custom header template") + lineTemplatePath = *flag.String("line", "", "Use a custom line template") + footerTemplatePath = *flag.String("footer", "", "Use a custom footer template") srcDir = defaultSrc destDir = defaultDest flag.Parse() @@ -518,8 +534,8 @@ func main() { // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose logState() - srcDir = abs(srcDir) - destDir = abs(destDir) + srcDir = getAbsPath(srcDir) + destDir = getAbsPath(destDir) if err = sanitizeDirectories(); err != nil { log.Fatal("Error while checking src and dest paths", err) } @@ -540,9 +556,11 @@ func main() { minifier.AddFunc("text/html", html.Minify) minifier.AddFunc("application/javascript", js.Minify) - if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { - log.Fatal("Could not parse header template", err) - } + loadTemplateOld("header", headerTemplatePath, &headerTemplate, &header) + + // if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { + // log.Fatal("Could not parse header template", err) + // } if line, err = loadTemplate("line", lineTemplatePath, &lineTemplate); err != nil { log.Fatal("Could not parse line template", err) } @@ -552,11 +570,13 @@ func main() { if err = readIfNotEmpty(styleTemplatePath, &style); err != nil { log.Fatal("Could not read stylesheet file", err) } + // ENDREGION INITGLOBALS - dir, err := walk(srcDir, isRecursive) + dir, err := walk(srcDir) if err != nil { log.Fatalf("Error when creating the directory structure:\n%s\n", err) } - - writeHTML(dir, []string{}) + // DEBUG REMOVE ME AFTER FIX + fmt.Printf("%+v\n", dir) + writeHTML(&dir, []string{}) } From 2b7b0b9f0770bdf329b462fd4f5527adb468b0cd Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 17 Jun 2022 16:40:14 +0200 Subject: [PATCH 06/16] fix --- statik.go | 96 ++++++++++++++++--------------------------------------- 1 file changed, 28 insertions(+), 68 deletions(-) diff --git a/statik.go b/statik.go index e2d21cf..62e95fb 100644 --- a/statik.go +++ b/statik.go @@ -31,20 +31,20 @@ import ( // Describes the state of every main variable of the program var ( //go:embed "header.gohtml" - headerTemplate string + headerTemplate string //go:embed "line.gohtml" - lineTemplate string + lineTemplate string //go:embed "footer.gohtml" - footerTemplate string + footerTemplate string //go:embed "style.css" style string header, footer, line *template.Template minifier *minify.M - styleTemplatePath string - headerTemplatePath string - lineTemplatePath string - footerTemplatePath string + styleTemplatePath string + headerTemplatePath string + lineTemplatePath string + footerTemplatePath string workDir string srcDir string @@ -59,7 +59,7 @@ var ( includeRegExStr string excludeRegExStr string baseURL *url.URL - rawURL string + rawURL string ) const ( @@ -92,20 +92,6 @@ type Line struct { Date time.Time } -// this interface will be used to handle the template programming -// every gohtml template should implement this interface -// TODO: this template interface could fit well with Registrary -// design pattern: everytime you have to use a new template -// you just register it (aka implement needed functions)! -// PROBLEM: i don't have any idea how to make it, i don't even know -// if it could be a good choice -type Template interface { - Data(interface{}) interface{} // the interface that this template builds upon - Load(string) // load the teplate - Raw() string // the default filepath for the template - Tmpl() *template.Template // just return the template pointer -} - type Named interface { GetName() string } @@ -113,7 +99,7 @@ type Named interface { type Directory struct { Name string `json:"name"` Path string `json:"path"` - SourcePath string `json:"-"` + SourcePath string `json:"-"` Directories []Directory `json:"directories"` Files []File `json:"files"` Size int64 `json:"size"` @@ -135,6 +121,7 @@ type File struct { } func (d Directory) GetName() string { return d.Name } +func (d Directory) isEmpty() bool { return len(d.Directories) == 0 && len(d.Files) == 0 } func (f File) GetName() string { return f.FuzzyFile.Name } // joins the baseURL with the given relative path in a new URL instance @@ -165,11 +152,11 @@ func newFile(info os.FileInfo, dir string) (f File, err error) { return File{ FuzzyFile: FuzzyFile{ - Name: info.Name(), - Path: rel, + Name: info.Name(), + Path: rel, SourcePath: dir, - URL: url, - Mime: mime.String(), + URL: url, + Mime: mime.String(), }, Size: info.Size(), ModTime: info.ModTime(), @@ -187,10 +174,6 @@ func isDir(path string) (err error) { return nil } -func (d Directory) isEmpty() bool { - return len(d.Directories) == 0 && len(d.Files) == 0 -} - func sortAlpha[T Named](infos []T) { sort.Slice(infos, func(i, j int) bool { return infos[i].GetName() < infos[j].GetName() @@ -199,11 +182,11 @@ func sortAlpha[T Named](infos []T) { func walk(base string) (dir Directory, err error) { var ( - infos []fs.FileInfo - sourceInfo fs.FileInfo - subdir Directory - file File - rel string + infos []fs.FileInfo + sourceInfo fs.FileInfo + subdir Directory + file File + rel string ) if infos, err = ioutil.ReadDir(base); err != nil { return dir, fmt.Errorf("Could not read directory %s:\n%s", base, err) @@ -218,11 +201,11 @@ func walk(base string) (dir Directory, err error) { } dir = Directory{ - Name: sourceInfo.Name(), + Name: sourceInfo.Name(), SourcePath: base, - Path: rel, - Size: sourceInfo.Size(), - ModTime: sourceInfo.ModTime(), + Path: rel, + Size: sourceInfo.Size(), + ModTime: sourceInfo.ModTime(), } for _, info := range infos { @@ -415,8 +398,8 @@ func readIfNotEmpty(path string, dest *string) (err error) { if err != nil { return fmt.Errorf("Could not read file: %s\n%s", path, err) } + *dest = string(content) } - *dest = string(content) return nil } @@ -427,29 +410,9 @@ func loadTemplate(name string, path string, buf *string) (tmpl *template.Templat if tmpl, err = template.New(name).Parse(*buf); err != nil { return } - return -} - -// TODO: debug: remove me after fix! -func loadTemplateOld(name string, path string, def *string, dest **template.Template) { - var ( - content []byte - err error - ) - if path != "" { - content, err = ioutil.ReadFile(path) - if err != nil { - log.Fatalf("Could not read %s template file %s:\n%s\n", name, path, err) - } - *def = string(content) - } - *dest, err = template.New(name).Parse(*def) - if err != nil { - log.Fatalf("Could not parse %s template %s:\n%s\n", name, path, err) - } + return } - func logState() { log.Println("Running with parameters:") log.Println("\tInclude:\t", includeRegExStr) @@ -499,7 +462,6 @@ func sanitizeDirectories() (err error) { } func main() { - // REGION INITGLOBALS var err error includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") @@ -556,11 +518,9 @@ func main() { minifier.AddFunc("text/html", html.Minify) minifier.AddFunc("application/javascript", js.Minify) - loadTemplateOld("header", headerTemplatePath, &headerTemplate, &header) - - // if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { - // log.Fatal("Could not parse header template", err) - // } + if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { + log.Fatal("Could not parse header template", err) + } if line, err = loadTemplate("line", lineTemplatePath, &lineTemplate); err != nil { log.Fatal("Could not parse line template", err) } From aab29790af5adc80b8d97bb5ce3cd5a1b9a2eddb Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 14:00:51 +0200 Subject: [PATCH 07/16] wip --- statik.go | 468 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 242 insertions(+), 226 deletions(-) diff --git a/statik.go b/statik.go index 62e95fb..6b74f0a 100644 --- a/statik.go +++ b/statik.go @@ -41,31 +41,25 @@ var ( header, footer, line *template.Template minifier *minify.M - styleTemplatePath string - headerTemplatePath string - lineTemplatePath string - footerTemplatePath string - workDir string srcDir string - destDir string - - isRecursive bool - isEmpty bool - enableSort bool - convertLink bool - includeRegEx *regexp.Regexp - excludeRegEx *regexp.Regexp - includeRegExStr string - excludeRegExStr string - baseURL *url.URL - rawURL string + dstDir string + + isRecursive bool + isEmpty bool + enableSort bool + convertLink bool + includeRegEx *regexp.Regexp + excludeRegEx *regexp.Regexp + baseURL *url.URL ) const ( linkSuffix = ".link" + linkMIME = "text/statik-link" + regularFile = os.FileMode(0666) defaultSrc = "./" - defaultDest = "site" + defaultDst = "site" ) type Dir struct { @@ -99,24 +93,29 @@ type Named interface { type Directory struct { Name string `json:"name"` Path string `json:"path"` - SourcePath string `json:"-"` + SrcPath string `json:"-"` + DstPath string `json:"-"` + URL *url.URL `json:"url"` Directories []Directory `json:"directories"` Files []File `json:"files"` - Size int64 `json:"size"` + Size string `json:"size"` ModTime time.Time `json:"time"` + Mode fs.FileMode `json:"-"` } type FuzzyFile struct { - Name string `json:"name"` - Path string `json:"path"` - SourcePath string `json:"-"` - URL *url.URL `json:"url"` - Mime string `json:"mime"` + Name string `json:"name"` + Path string `json:"path"` + SrcPath string `json:"-"` + DstPath string `json:"-"` + URL *url.URL `json:"url"` + MIME *mimetype.MIME `json:"mime"` + Mode fs.FileMode `json:"-"` } type File struct { FuzzyFile - Size int64 `json:"size"` + Size string `json:"size"` ModTime time.Time `json:"time"` } @@ -131,36 +130,34 @@ func withBaseURL(rel string) (url *url.URL) { return } -func newFile(info os.FileInfo, dir string) (f File, err error) { - if info.IsDir() { - return File{}, errors.New("newFile has been called with a os.FileInfo of type Directory") +func getAbsPath(rel string) string { + if filepath.IsAbs(rel) { + return rel } - var ( - rel string - url *url.URL - mime *mimetype.MIME - ) - abs := path.Join(dir, info.Name()) - if rel, err = filepath.Rel(srcDir, abs); err != nil { + return path.Join(workDir, rel) +} + +func readIfNotEmpty(path string, dst *string) (err error) { + var content []byte + if path != "" { + content, err = ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("Could not read file: %s\n%s", path, err) + } + *dst = string(content) + } + return nil +} + +func loadTemplate(name string, path string, buf *string) (tmpl *template.Template, err error) { + if err = readIfNotEmpty(path, buf); err != nil { return } - url = withBaseURL(rel) - if mime, err = mimetype.DetectFile(abs); err != nil { + if tmpl, err = template.New(name).Parse(*buf); err != nil { return } - - return File{ - FuzzyFile: FuzzyFile{ - Name: info.Name(), - Path: rel, - SourcePath: dir, - URL: url, - Mime: mime.String(), - }, - Size: info.Size(), - ModTime: info.ModTime(), - }, nil + return } func isDir(path string) (err error) { @@ -174,83 +171,131 @@ func isDir(path string) (err error) { return nil } -func sortAlpha[T Named](infos []T) { +// The input path dir is assumed to be already absolute +func newFile(info os.FileInfo, dir string) (fz FuzzyFile, f File, err error) { + if info.IsDir() { + return fz, File{}, errors.New("newFile has been called with a os.FileInfo of type Directory") + } + + var ( + rel string + mime *mimetype.MIME + ) + abs := path.Join(dir, info.Name()) + if rel, err = filepath.Rel(srcDir, abs); err != nil { + return + } + if mime, err = mimetype.DetectFile(abs); err != nil { + return + } + + fz = FuzzyFile{ + Name: info.Name(), + Path: rel, + SrcPath: abs, + DstPath: path.Join(dstDir, rel), + URL: withBaseURL(rel), + MIME: mime, + Mode: info.Mode(), + } + return fz, File{ + FuzzyFile: fz, + Size: humanize.Bytes(uint64(info.Size())), + ModTime: info.ModTime(), + }, nil +} + +func sortByName[T Named](infos []T) { sort.Slice(infos, func(i, j int) bool { return infos[i].GetName() < infos[j].GetName() }) } -func walk(base string) (dir Directory, err error) { +func includeDir(info fs.FileInfo) bool { + return !excludeRegEx.MatchString(info.Name()) +} + +func includeFile(info fs.FileInfo) bool { + return includeRegEx.MatchString(info.Name()) && !excludeRegEx.MatchString(info.Name()) +} + +func walk(base string) (dir Directory, fz []FuzzyFile, err error) { + // Avoid infinite recursion over the destination directory + if base == dstDir { + return + } + var ( - infos []fs.FileInfo - sourceInfo fs.FileInfo - subdir Directory - file File - rel string + infos []fs.FileInfo + dirInfo fs.FileInfo + subdir Directory + subfz []FuzzyFile + file File + fuzzy FuzzyFile + rel string ) if infos, err = ioutil.ReadDir(base); err != nil { - return dir, fmt.Errorf("Could not read directory %s:\n%s", base, err) + return dir, fz, fmt.Errorf("Could not read directory %s:\n%s", base, err) } - if sourceInfo, err = os.Stat(base); err != nil { - return dir, fmt.Errorf("Could not stat directory %s:\n%s", base, err) + if dirInfo, err = os.Stat(base); err != nil { + return dir, fz, fmt.Errorf("Could not stat directory %s:\n%s", base, err) } if rel, err = filepath.Rel(srcDir, base); err != nil { - return dir, err + return } dir = Directory{ - Name: sourceInfo.Name(), - SourcePath: base, - Path: rel, - Size: sourceInfo.Size(), - ModTime: sourceInfo.ModTime(), + Name: dirInfo.Name(), + SrcPath: base, + DstPath: path.Join(dstDir, rel), + URL: withBaseURL(rel), + Path: rel, + Size: humanize.Bytes(uint64(dirInfo.Size())), + ModTime: dirInfo.ModTime(), + Mode: dirInfo.Mode(), } for _, info := range infos { if info.IsDir() && isRecursive && includeDir(info) { - if subdir, err = walk(path.Join(base, info.Name())); err != nil { - return dir, err + if subdir, subfz, err = walk(path.Join(base, info.Name())); err != nil { + return } if !subdir.isEmpty() || isEmpty { // include emptydir if isEmptyflag is setted dir.Directories = append(dir.Directories, subdir) + fz = append(fz, subfz...) } } else if !info.IsDir() && includeFile(info) { - if file, err = newFile(info, base); err != nil { - return dir, fmt.Errorf("Error while generating the File structure:\n%s", err) + if fuzzy, file, err = newFile(info, base); err != nil { + return dir, fz, fmt.Errorf("Error while generating the File structure:\n%s", err) } + fz = append(fz, fuzzy) dir.Files = append(dir.Files, file) } } - sortAlpha(dir.Files) - sortAlpha(dir.Directories) + if enableSort { + sortByName(dir.Files) + sortByName(dir.Directories) + } return } func gen(tmpl *template.Template, data interface{}, out io.Writer) { if err := tmpl.Execute(out, data); err != nil { - log.Fatalf("could not generate template for the %s section:\n%s\n", tmpl.Name(), err) + log.Fatalf("Could not generate template for the %s section:\n%s\n", tmpl.Name(), err) } } -func copy(src, dest string) { - input, err := ioutil.ReadFile(src) - if err != nil { - log.Fatalf("Could not open SrcDir file for copying: %s\n%s\n", src, err) +func copy(f FuzzyFile) (err error) { + var input []byte + if input, err = ioutil.ReadFile(f.SrcPath); err != nil { + return fmt.Errorf("Could not open %s for reading:\n%s", f.SrcPath, err) } - err = ioutil.WriteFile(dest, input, 0644) - if err != nil { - log.Fatalf("Could not write to destination file for copying: %s\n%s\n", dest, err) + if err = ioutil.WriteFile(f.DstPath, input, f.Mode); err != nil { + return fmt.Errorf("Could not open %s for writing:\n%s", f.DstPath, err) } -} - -func includeDir(info fs.FileInfo) bool { - return !excludeRegEx.MatchString(info.Name()) -} - -func includeFile(info fs.FileInfo) bool { - return includeRegEx.MatchString(info.Name()) && !excludeRegEx.MatchString(info.Name()) + return nil } // REGION GENERATE @@ -258,8 +303,8 @@ func includeFile(info fs.FileInfo) bool { // the function parameters are temporary, i have to find a way to reduce it... // Generate the header and the double dots back anchor when appropriate -func generateHeader(parts []string, outBuff *bytes.Buffer) { - rel := path.Join(parts...) +func generateHeader(dir Directory, outBuff io.Writer) { + parts := filepath.SplitList(dir.Path) p, url := []Dir{}, "" for _, part := range parts { url = path.Join(url, part) @@ -267,117 +312,109 @@ func generateHeader(parts []string, outBuff *bytes.Buffer) { } gen(header, Header{ Root: Dir{ - Name: strings.TrimPrefix(strings.TrimSuffix(baseURL.Path, "/"), "/"), + Name: dir.Name, URL: baseURL.String(), }, Parts: p, - FullPath: path.Join(baseURL.Path+rel) + "/", + FullPath: dir.URL.Path + "/", Stylesheet: template.CSS(style), }, outBuff) } // populate the back line -func generateBackLine(parts []string, outBuff *bytes.Buffer) { - rel := path.Join(parts...) - if len(parts) != 0 { +func generateBackLine(dir Directory, outBuff *bytes.Buffer) { + log.Println("PATHHHHH: ", dir.Path) + if dir.Path != "." { gen(line, Line{ IsDir: true, Name: "..", - URL: withBaseURL(path.Join(rel, "..")).Path, + URL: dir.URL.Path, Size: humanize.Bytes(0), }, outBuff) } } -func generateDirectories(dirs []Directory, parts []string, outBuff *bytes.Buffer) { - rel := path.Join(parts...) - dirName := path.Join(srcDir, rel) - outDir := path.Join(destDir, rel) - for _, dirEntry := range dirs { - dirPath := path.Join(dirName, dirEntry.Name) - // Avoid recursive infinite loop - if dirPath == outDir { - continue - } - +func generateDirectories(dirs []Directory, outBuff *bytes.Buffer) { + for _, dir := range dirs { data := Line{ IsDir: true, - Name: dirEntry.Name, - URL: withBaseURL(path.Join(rel, dirEntry.Name)).Path, - Size: humanize.Bytes(uint64(dirEntry.Size)), - Date: dirEntry.ModTime, + Name: dir.Name, + URL: dir.URL.Path, + Size: dir.Size, + Date: dir.ModTime, } - writeHTML(&dirEntry, append(parts, dirEntry.Name)) + writeHTML(dir) gen(line, data, outBuff) } } -func generateFiles(files []File, parts []string, outBuff *bytes.Buffer) { - rel := path.Join(parts...) - dirName := path.Join(srcDir, rel) - outDir := path.Join(destDir, rel) - for _, fileEntry := range files { - - // DEBUG REMOVE ME AFTER FIX - fmt.Printf("file probably generated: %s \n", fileEntry.Name) - filePath := path.Join(dirName, fileEntry.Name) +func generateFiles(files []File, outBuff *bytes.Buffer) { + for _, file := range files { + fmt.Printf("file probably generated: %s \n", file.Name) data := Line{ IsDir: false, - Name: fileEntry.Name, - URL: withBaseURL(path.Join(rel, fileEntry.Name)).Path, - Size: humanize.Bytes(uint64(fileEntry.Size)), - Date: fileEntry.ModTime, + Name: file.Name, + URL: file.URL.Path, + Size: file.Size, + Date: file.ModTime, } - if strings.HasSuffix(filePath, linkSuffix) { + if file.MIME.Is(linkMIME) { data.Name = data.Name[:len(data.Name)-len(linkSuffix)] // get name without extension data.Size = humanize.Bytes(0) - raw, err := ioutil.ReadFile(filePath) + raw, err := ioutil.ReadFile(file.SrcPath) if err != nil { - log.Fatalf("Could not read link file: %s\n%s\n", filePath, err) + log.Fatalf("Could not read link file: %s\n%s\n", file.SrcPath, err) } rawStr := string(raw) u, err := url.Parse(strings.TrimSpace(rawStr)) if err != nil { - log.Fatalf("Could not parse URL in file: %s\nThe value is: %s\n%s\n", filePath, raw, err) + log.Fatalf("Could not parse URL in file: %s\nThe value is: %s\n%s\n", file.SrcPath, raw, err) } data.URL = u.String() gen(line, data, outBuff) - } else { - // Copy all files over to the web root - copy(filePath, path.Join(outDir, fileEntry.Name)) } gen(line, data, outBuff) } } -// END REGION GENERATE - -func writeHTML(directory *Directory, parts []string) { - rel := path.Join(parts...) - srcDirName := path.Join(srcDir, rel) - outDir := path.Join(destDir, rel) - - // CHECK IF OUTPUT DIRECTORY HAS GOOD PERMS - err := os.Mkdir(outDir, os.ModePerm) - if err != nil { - log.Fatalf("Could not create output *sub*directory: %s\n%s\n", outDir, err) +func writeCopies(dir Directory, fz []FuzzyFile) (err error) { + dirs := append([]Directory{dir}, dir.Directories...) + for _, d := range dirs { + dirs = append(dirs, d.Directories...) + if err = os.MkdirAll(d.DstPath, os.ModeDir|os.ModePerm); err != nil { + return fmt.Errorf("Could not create output directory %s:\n%s", d.DstPath, err) + } + } + for _, f := range fz { + if f.MIME.Is(linkMIME) { + continue + } + if err = copy(f); err != nil { + return err + } } + return nil +} + +func writeJSON(dir Directory, fz []FuzzyFile) error { + return nil +} - // CREATE HTMLFILE - htmlPath := path.Join(outDir, "index.html") - html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, 0666) +func writeHTML(dir Directory) error { + htmlPath := path.Join(dir.DstPath, "index.html") + html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, regularFile) if err != nil { log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err) } out := new(bytes.Buffer) - generateHeader(parts, out) - generateBackLine(parts, out) - generateDirectories(directory.Directories, parts, out) - generateFiles(directory.Files, parts, out) + generateHeader(dir, out) + generateBackLine(dir, out) + generateDirectories(dir.Directories, out) + generateFiles(dir.Files, out) gen(footer, Footer{Date: time.Now()}, out) err = minifier.Minify("text/html", html, out) @@ -388,61 +425,28 @@ func writeHTML(directory *Directory, parts []string) { if err != nil { log.Fatalf("Could not write to close index.html: %s\n%s\n", htmlPath, err) } - log.Printf("Generated data for directory: %s\n", srcDirName) -} - -func readIfNotEmpty(path string, dest *string) (err error) { - var content []byte - if path != "" { - content, err = ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("Could not read file: %s\n%s", path, err) - } - *dest = string(content) - } + log.Printf("Generated data for directory: %s\n", dir.Name) return nil } -func loadTemplate(name string, path string, buf *string) (tmpl *template.Template, err error) { - if err = readIfNotEmpty(path, buf); err != nil { - return - } - if tmpl, err = template.New(name).Parse(*buf); err != nil { - return - } - return -} - func logState() { log.Println("Running with parameters:") - log.Println("\tInclude:\t", includeRegExStr) - log.Println("\tExclude:\t", excludeRegExStr) + log.Println("\tInclude:\t", includeRegEx.String()) + log.Println("\tExclude:\t", excludeRegEx.String()) log.Println("\tRecursive:\t", isRecursive) log.Println("\tEmpty:\t\t", isEmpty) log.Println("\tConvert links:\t", convertLink) log.Println("\tSource:\t\t", srcDir) - log.Println("\tDestination:\t", destDir) - log.Println("\tBase URL:\t", rawURL) - log.Println("\tStyle:\t\t", styleTemplatePath) - log.Println("\tFooter:\t\t", footerTemplatePath) - log.Println("\tHeader:\t\t", headerTemplatePath) - log.Println("\tline:\t\t", lineTemplatePath) -} - -func getAbsPath(rel string) string { - if filepath.IsAbs(rel) { - return rel - } - - return path.Join(workDir, rel) + log.Println("\tDstination:\t", dstDir) + log.Println("\tBase URL:\t", baseURL.String()) } func sanitizeDirectories() (err error) { - if strings.HasPrefix(srcDir, destDir) { + if strings.HasPrefix(srcDir, dstDir) { return errors.New("The output directory cannot be a parent of the input directory") } - if _, err = os.OpenFile(srcDir, os.O_RDONLY, 0666); err != nil && os.IsPermission(err) { + if _, err = os.OpenFile(srcDir, os.O_RDONLY, os.ModeDir|os.ModePerm); err != nil && os.IsPermission(err) { return fmt.Errorf("Cannot open source directory for reading: %s\n%s", srcDir, err) } @@ -451,43 +455,48 @@ func sanitizeDirectories() (err error) { } // check if outputDir is writable - if _, err = os.OpenFile(destDir, os.O_WRONLY, 0666); err != nil && os.IsPermission(err) { - return fmt.Errorf("Cannot open output directory for writing: %s\n%s", destDir, err) + if _, err = os.OpenFile(dstDir, os.O_WRONLY, os.ModeDir|os.ModePerm); err != nil && os.IsPermission(err) { + return fmt.Errorf("Cannot open output directory for writing: %s\n%s", dstDir, err) } - if err = os.RemoveAll(destDir); err != nil { - return fmt.Errorf("Cannot clear output directory: %s\n%s", destDir, err) + if err = os.RemoveAll(dstDir); err != nil { + return fmt.Errorf("Cannot clear output directory: %s\n%s", dstDir, err) } return nil } func main() { var err error - includeRegExStr = *flag.String("i", ".*", "A regex pattern to include files into the listing") - excludeRegExStr = *flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") - isRecursive = *flag.Bool("r", true, "Recursively scan the file tree") - isEmpty = *flag.Bool("empty", false, "Whether to list empty directories") - enableSort = *flag.Bool("sort", true, "Sort files A-z and by type") - rawURL = *flag.String("b", "http://localhost", "The base URL") - convertLink = *flag.Bool("l", false, "Convert .link files to anchor tags") - styleTemplatePath = *flag.String("style", "", "Use a custom stylesheet file") - headerTemplatePath = *flag.String("header", "", "Use a custom header template") - lineTemplatePath = *flag.String("line", "", "Use a custom line template") - footerTemplatePath = *flag.String("footer", "", "Use a custom footer template") - srcDir = defaultSrc - destDir = defaultDest + includeRegExStr := flag.String("i", ".*", "A regex pattern to include files into the listing") + excludeRegExStr := flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") + _isRecursive := flag.Bool("r", true, "Recursively scan the file tree") + _isEmpty := flag.Bool("empty", false, "Whether to list empty directories") + _enableSort := flag.Bool("sort", true, "Sort files A-z and by type") + rawURL := flag.String("b", "http://localhost", "The base URL") + _convertLink := flag.Bool("l", false, "Convert .link files to anchor tags") + styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") + headerTemplatePath := flag.String("header", "", "Use a custom header template") + lineTemplatePath := flag.String("line", "", "Use a custom line template") + footerTemplatePath := flag.String("footer", "", "Use a custom footer template") flag.Parse() + srcDir = defaultSrc + dstDir = defaultDst + isRecursive = *_isRecursive + isEmpty = *_isEmpty + enableSort = *_enableSort + convertLink = *_convertLink + args := flag.Args() - if len(args) > 2 { - fmt.Printf("Usage: %s [dest] or [src] [dest]\n", os.Args[0]) + if len(args) < 1 { + fmt.Printf("Usage: %s [dst] or [src] [dst]\n", os.Args[0]) os.Exit(1) } if len(args) == 1 { - destDir = args[0] + dstDir = args[0] } else if len(args) == 2 { srcDir = args[0] - destDir = args[1] + dstDir = args[1] } if workDir, err = os.Getwd(); err != nil { @@ -495,48 +504,55 @@ func main() { } // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose - logState() srcDir = getAbsPath(srcDir) - destDir = getAbsPath(destDir) + dstDir = getAbsPath(dstDir) if err = sanitizeDirectories(); err != nil { - log.Fatal("Error while checking src and dest paths", err) + log.Fatal("Error while checking src and dst paths", err) } - if includeRegEx, err = regexp.Compile(includeRegExStr); err != nil { + if includeRegEx, err = regexp.Compile(*includeRegExStr); err != nil { log.Fatal("Invalid regexp for include matching", err) } - if excludeRegEx, err = regexp.Compile(excludeRegExStr); err != nil { + if excludeRegEx, err = regexp.Compile(*excludeRegExStr); err != nil { log.Fatal("Invalid regexp for exclude matching", err) } - if baseURL, err = url.Parse(rawURL); err != nil { + if baseURL, err = url.Parse(*rawURL); err != nil { log.Fatal("Could not parse base URL", err) } + logState() + mimetype.Lookup("text/plain").Extend(func(_ []byte, size uint32) bool { return true }, linkMIME, ".link") minifier = minify.New() minifier.AddFunc("text/css", css.Minify) minifier.AddFunc("text/html", html.Minify) minifier.AddFunc("application/javascript", js.Minify) - if header, err = loadTemplate("header", headerTemplatePath, &headerTemplate); err != nil { + if header, err = loadTemplate("header", *headerTemplatePath, &headerTemplate); err != nil { log.Fatal("Could not parse header template", err) } - if line, err = loadTemplate("line", lineTemplatePath, &lineTemplate); err != nil { + if line, err = loadTemplate("line", *lineTemplatePath, &lineTemplate); err != nil { log.Fatal("Could not parse line template", err) } - if footer, err = loadTemplate("footer", footerTemplatePath, &footerTemplate); err != nil { + if footer, err = loadTemplate("footer", *footerTemplatePath, &footerTemplate); err != nil { log.Fatal("Could not parse footer template", err) } - if err = readIfNotEmpty(styleTemplatePath, &style); err != nil { + if err = readIfNotEmpty(*styleTemplatePath, &style); err != nil { log.Fatal("Could not read stylesheet file", err) } - // ENDREGION INITGLOBALS - dir, err := walk(srcDir) + dir, fz, err := walk(srcDir) if err != nil { log.Fatalf("Error when creating the directory structure:\n%s\n", err) } - // DEBUG REMOVE ME AFTER FIX - fmt.Printf("%+v\n", dir) - writeHTML(&dir, []string{}) + if err = writeCopies(dir, fz); err != nil { + log.Fatalf("Error while copying included files to the destination:\n%s\n", err) + } + + if err = writeJSON(dir, fz); err != nil { + log.Fatalf("Error while generating JSON metadata:\n%s\n", err) + } + if err = writeHTML(dir); err != nil { + log.Fatalf("Error while generating JSON metadata:\n%s\n", err) + } } From 1cbdf9401b0944215a974b1f4fbec065ee9df7c2 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 15:09:57 +0200 Subject: [PATCH 08/16] finalize json support --- statik.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/statik.go b/statik.go index 6b74f0a..fd64f95 100644 --- a/statik.go +++ b/statik.go @@ -3,6 +3,7 @@ package main import ( "bytes" _ "embed" + "encoding/json" "errors" "flag" "fmt" @@ -52,14 +53,18 @@ var ( includeRegEx *regexp.Regexp excludeRegEx *regexp.Regexp baseURL *url.URL + + linkMIME *mimetype.MIME ) const ( linkSuffix = ".link" - linkMIME = "text/statik-link" regularFile = os.FileMode(0666) defaultSrc = "./" defaultDst = "site" + + fuzzyFileName = "fuzzy.json" + metadataFileName = "statik.json" ) type Dir struct { @@ -103,6 +108,17 @@ type Directory struct { Mode fs.FileMode `json:"-"` } +func (d *Directory) MarshalJSON() ([]byte, error) { + type DirectoryAlias Directory + return json.Marshal(&struct { + URL string `json:"url"` + *DirectoryAlias + }{ + URL: d.URL.String(), + DirectoryAlias: (*DirectoryAlias)(d), + }) +} + type FuzzyFile struct { Name string `json:"name"` Path string `json:"path"` @@ -113,12 +129,38 @@ type FuzzyFile struct { Mode fs.FileMode `json:"-"` } +func (f *FuzzyFile) MarshalJSON() ([]byte, error) { + type FuzzyFileAlias FuzzyFile + return json.Marshal(&struct { + URL string `json:"url"` + MIME string `json:"mime"` + *FuzzyFileAlias + }{ + URL: f.URL.String(), + MIME: f.MIME.String(), + FuzzyFileAlias: (*FuzzyFileAlias)(f), + }) +} + type File struct { FuzzyFile Size string `json:"size"` ModTime time.Time `json:"time"` } +func (f *File) MarshalJSON() ([]byte, error) { + type FileAlias File + return json.Marshal(&struct { + URL string `json:"url"` + MIME string `json:"mime"` + *FileAlias + }{ + URL: f.FuzzyFile.URL.String(), + MIME: f.FuzzyFile.MIME.String(), + FileAlias: (*FileAlias)(f), + }) +} + func (d Directory) GetName() string { return d.Name } func (d Directory) isEmpty() bool { return len(d.Directories) == 0 && len(d.Files) == 0 } func (f File) GetName() string { return f.FuzzyFile.Name } @@ -185,7 +227,9 @@ func newFile(info os.FileInfo, dir string) (fz FuzzyFile, f File, err error) { if rel, err = filepath.Rel(srcDir, abs); err != nil { return } - if mime, err = mimetype.DetectFile(abs); err != nil { + if strings.HasSuffix(info.Name(), linkSuffix) { + mime = linkMIME + } else if mime, err = mimetype.DetectFile(abs); err != nil { return } @@ -359,7 +403,7 @@ func generateFiles(files []File, outBuff *bytes.Buffer) { Size: file.Size, Date: file.ModTime, } - if file.MIME.Is(linkMIME) { + if file.MIME == linkMIME { data.Name = data.Name[:len(data.Name)-len(linkSuffix)] // get name without extension data.Size = humanize.Bytes(0) @@ -384,12 +428,12 @@ func writeCopies(dir Directory, fz []FuzzyFile) (err error) { dirs := append([]Directory{dir}, dir.Directories...) for _, d := range dirs { dirs = append(dirs, d.Directories...) - if err = os.MkdirAll(d.DstPath, os.ModeDir|os.ModePerm); err != nil { + if err = os.MkdirAll(d.DstPath, d.Mode); err != nil { return fmt.Errorf("Could not create output directory %s:\n%s", d.DstPath, err) } } for _, f := range fz { - if f.MIME.Is(linkMIME) { + if f.MIME == linkMIME { continue } if err = copy(f); err != nil { @@ -399,7 +443,36 @@ func writeCopies(dir Directory, fz []FuzzyFile) (err error) { return nil } -func writeJSON(dir Directory, fz []FuzzyFile) error { +func jsonToFile[T any](path string, v T) (err error) { + var data []byte + if data, err = json.Marshal(&v); err != nil { + return fmt.Errorf("Could not serialize JSON:\n%s", err) + } + if err = ioutil.WriteFile(path, data, regularFile); err != nil { + return fmt.Errorf("Could not write metadata file %s:\n%s", path, err) + } + return nil +} + +func writeJSON(dir Directory, fz []FuzzyFile) (err error) { + // Write the fuzzy.json file in the root directory + if len(fz) != 0 { + if err = jsonToFile(path.Join(dir.DstPath, fuzzyFileName), fz); err != nil { + return + } + } + + // Write the directory metadata + if err = jsonToFile(path.Join(dir.DstPath, metadataFileName), dir); err != nil { + return + } + + for _, d := range dir.Directories { + if err = writeJSON(d, []FuzzyFile{}); err != nil { + return + } + } + return nil } @@ -409,6 +482,7 @@ func writeHTML(dir Directory) error { if err != nil { log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err) } + defer html.Close() out := new(bytes.Buffer) generateHeader(dir, out) @@ -522,7 +596,13 @@ func main() { } logState() - mimetype.Lookup("text/plain").Extend(func(_ []byte, size uint32) bool { return true }, linkMIME, ".link") + // Ugly hack to generate our custom mime, there currently is no way around this + { + v := true + mimetype.Lookup("text/plain").Extend(func(_ []byte, size uint32) bool { return v }, "text/statik-link", ".link") + linkMIME = mimetype.Detect([]byte("some plain text")) + v = false + } minifier = minify.New() minifier.AddFunc("text/css", css.Minify) minifier.AddFunc("text/html", html.Minify) From e3cb0ac62fffe3ef5e9334cbaa82dba497a258ee Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 15:44:25 +0200 Subject: [PATCH 09/16] properly handle links --- statik.go | 67 +++++++++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/statik.go b/statik.go index fd64f95..5130276 100644 --- a/statik.go +++ b/statik.go @@ -73,9 +73,8 @@ type Dir struct { } type Header struct { - Root Dir - Parts []Dir - FullPath string + Directory Directory + Parts []Directory Stylesheet template.CSS } @@ -216,35 +215,50 @@ func isDir(path string) (err error) { // The input path dir is assumed to be already absolute func newFile(info os.FileInfo, dir string) (fz FuzzyFile, f File, err error) { if info.IsDir() { - return fz, File{}, errors.New("newFile has been called with a os.FileInfo of type Directory") + return fz, f, errors.New("newFile has been called with a os.FileInfo of type Directory") } var ( - rel string - mime *mimetype.MIME + rel, name, size string + raw []byte + url *url.URL + mime *mimetype.MIME ) abs := path.Join(dir, info.Name()) if rel, err = filepath.Rel(srcDir, abs); err != nil { return } + + url = withBaseURL(rel) + size = humanize.Bytes(uint64(info.Size())) + name = info.Name() if strings.HasSuffix(info.Name(), linkSuffix) { + if raw, err = ioutil.ReadFile(abs); err != nil { + return fz, f, fmt.Errorf("Could not read link file: %s\n%s\n", abs, err) + } + if url, err = url.Parse(strings.TrimSpace(string(raw))); err != nil { + return fz, f, fmt.Errorf("Could not parse URL in file %s\n: %s\n%s\n", abs, raw, err) + } + + size = humanize.Bytes(0) + name = name[:len(name)-len(linkSuffix)] mime = linkMIME } else if mime, err = mimetype.DetectFile(abs); err != nil { return } fz = FuzzyFile{ - Name: info.Name(), + Name: name, Path: rel, SrcPath: abs, DstPath: path.Join(dstDir, rel), - URL: withBaseURL(rel), + URL: url, MIME: mime, Mode: info.Mode(), } return fz, File{ FuzzyFile: fz, - Size: humanize.Bytes(uint64(info.Size())), + Size: size, ModTime: info.ModTime(), }, nil } @@ -348,19 +362,15 @@ func copy(f FuzzyFile) (err error) { // Generate the header and the double dots back anchor when appropriate func generateHeader(dir Directory, outBuff io.Writer) { - parts := filepath.SplitList(dir.Path) - p, url := []Dir{}, "" - for _, part := range parts { - url = path.Join(url, part) - p = append(p, Dir{Name: part, URL: withBaseURL(url).Path}) + relUrl := "" + parts := []Directory{} + for _, part := range filepath.SplitList(dir.Path) { + relUrl = path.Join(relUrl, part) + parts = append(parts, Directory{Name: part, URL: withBaseURL(relUrl)}) } gen(header, Header{ - Root: Dir{ - Name: dir.Name, - URL: baseURL.String(), - }, - Parts: p, - FullPath: dir.URL.Path + "/", + Directory: dir, + Parts: parts, Stylesheet: template.CSS(style), }, outBuff) } @@ -403,23 +413,6 @@ func generateFiles(files []File, outBuff *bytes.Buffer) { Size: file.Size, Date: file.ModTime, } - if file.MIME == linkMIME { - data.Name = data.Name[:len(data.Name)-len(linkSuffix)] // get name without extension - data.Size = humanize.Bytes(0) - - raw, err := ioutil.ReadFile(file.SrcPath) - if err != nil { - log.Fatalf("Could not read link file: %s\n%s\n", file.SrcPath, err) - } - rawStr := string(raw) - u, err := url.Parse(strings.TrimSpace(rawStr)) - if err != nil { - log.Fatalf("Could not parse URL in file: %s\nThe value is: %s\n%s\n", file.SrcPath, raw, err) - } - - data.URL = u.String() - gen(line, data, outBuff) - } gen(line, data, outBuff) } } From 373a413e3c8e555352c74778a2b2d80788892cd0 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 16:21:18 +0200 Subject: [PATCH 10/16] rework html generation --- page.gohtml | 28 ++++++++ statik.go | 191 ++++++++++++++-------------------------------------- 2 files changed, 80 insertions(+), 139 deletions(-) create mode 100644 page.gohtml diff --git a/page.gohtml b/page.gohtml new file mode 100644 index 0000000..a63f962 --- /dev/null +++ b/page.gohtml @@ -0,0 +1,28 @@ + + + + + + Index of {{ .Root.URL.Path }} + + +

+ Index of {{ if not (eq .Root.Path ".") }}/{{ .Root.Name }}{{ end }}/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }} +

+
+
+ {{ range $i,$d := .Root.Directories }} + {{ $d.Name }} +

{{ $d.ModTime.Format "02 Jan 06 15:04 MST" }}

+

{{ $d.Size }}

+ {{ end }} + {{ range $i,$f := .Root.Files }} + {{ $f.Name }} +

{{ $f.ModTime.Format "02 Jan 06 15:04 MST" }}

+

{{ $f.Size }}

+ {{ end }} +
+
+

Generated by statik on {{ .Today.Format "02 Jan 06 15:04 MST" }}

+ + diff --git a/statik.go b/statik.go index 5130276..a148096 100644 --- a/statik.go +++ b/statik.go @@ -8,7 +8,6 @@ import ( "flag" "fmt" "html/template" - "io" "io/fs" "io/ioutil" "log" @@ -29,25 +28,20 @@ import ( "github.com/tdewolff/minify/v2/js" ) -// Describes the state of every main variable of the program var ( - //go:embed "header.gohtml" - headerTemplate string - //go:embed "line.gohtml" - lineTemplate string - //go:embed "footer.gohtml" - footerTemplate string + //go:embed "page.gohtml" + pageTemplate string //go:embed "style.css" - style string - header, footer, line *template.Template - minifier *minify.M + style string + page *template.Template + minifier *minify.M workDir string srcDir string dstDir string isRecursive bool - isEmpty bool + includeEmpty bool enableSort bool convertLink bool includeRegEx *regexp.Regexp @@ -67,31 +61,11 @@ const ( metadataFileName = "statik.json" ) -type Dir struct { - Name string - URL string -} - -type Header struct { - Directory Directory +type HTMLPayload struct { Parts []Directory + Root Directory Stylesheet template.CSS -} - -type Footer struct { - Date time.Time -} - -type Line struct { - IsDir bool - Name string - URL string - Size string - Date time.Time -} - -type Named interface { - GetName() string + Today time.Time } type Directory struct { @@ -160,9 +134,7 @@ func (f *File) MarshalJSON() ([]byte, error) { }) } -func (d Directory) GetName() string { return d.Name } -func (d Directory) isEmpty() bool { return len(d.Directories) == 0 && len(d.Files) == 0 } -func (f File) GetName() string { return f.FuzzyFile.Name } +func (d Directory) isEmpty() bool { return len(d.Directories) == 0 && len(d.Files) == 0 } // joins the baseURL with the given relative path in a new URL instance func withBaseURL(rel string) (url *url.URL) { @@ -263,6 +235,13 @@ func newFile(info os.FileInfo, dir string) (fz FuzzyFile, f File, err error) { }, nil } +type Named interface { + GetName() string +} + +func (d Directory) GetName() string { return d.Name } +func (f File) GetName() string { return f.FuzzyFile.Name } + func sortByName[T Named](infos []T) { sort.Slice(infos, func(i, j int) bool { return infos[i].GetName() < infos[j].GetName() @@ -320,7 +299,8 @@ func walk(base string) (dir Directory, fz []FuzzyFile, err error) { if subdir, subfz, err = walk(path.Join(base, info.Name())); err != nil { return } - if !subdir.isEmpty() || isEmpty { // include emptydir if isEmptyflag is setted + if !subdir.isEmpty() || includeEmpty { + // include emptydir if isEmptyflag is setted dir.Directories = append(dir.Directories, subdir) fz = append(fz, subfz...) } @@ -339,12 +319,6 @@ func walk(base string) (dir Directory, fz []FuzzyFile, err error) { return } -func gen(tmpl *template.Template, data interface{}, out io.Writer) { - if err := tmpl.Execute(out, data); err != nil { - log.Fatalf("Could not generate template for the %s section:\n%s\n", tmpl.Name(), err) - } -} - func copy(f FuzzyFile) (err error) { var input []byte if input, err = ioutil.ReadFile(f.SrcPath); err != nil { @@ -356,67 +330,6 @@ func copy(f FuzzyFile) (err error) { return nil } -// REGION GENERATE -// TODO: these functions should be later generalized with interface and so on... -// the function parameters are temporary, i have to find a way to reduce it... - -// Generate the header and the double dots back anchor when appropriate -func generateHeader(dir Directory, outBuff io.Writer) { - relUrl := "" - parts := []Directory{} - for _, part := range filepath.SplitList(dir.Path) { - relUrl = path.Join(relUrl, part) - parts = append(parts, Directory{Name: part, URL: withBaseURL(relUrl)}) - } - gen(header, Header{ - Directory: dir, - Parts: parts, - Stylesheet: template.CSS(style), - }, outBuff) -} - -// populate the back line -func generateBackLine(dir Directory, outBuff *bytes.Buffer) { - log.Println("PATHHHHH: ", dir.Path) - if dir.Path != "." { - gen(line, Line{ - IsDir: true, - Name: "..", - URL: dir.URL.Path, - Size: humanize.Bytes(0), - }, outBuff) - } -} - -func generateDirectories(dirs []Directory, outBuff *bytes.Buffer) { - for _, dir := range dirs { - data := Line{ - IsDir: true, - Name: dir.Name, - URL: dir.URL.Path, - Size: dir.Size, - Date: dir.ModTime, - } - - writeHTML(dir) - gen(line, data, outBuff) - } -} - -func generateFiles(files []File, outBuff *bytes.Buffer) { - for _, file := range files { - fmt.Printf("file probably generated: %s \n", file.Name) - data := Line{ - IsDir: false, - Name: file.Name, - URL: file.URL.Path, - Size: file.Size, - Date: file.ModTime, - } - gen(line, data, outBuff) - } -} - func writeCopies(dir Directory, fz []FuzzyFile) (err error) { dirs := append([]Directory{dir}, dir.Directories...) for _, d := range dirs { @@ -469,30 +382,38 @@ func writeJSON(dir Directory, fz []FuzzyFile) (err error) { return nil } -func writeHTML(dir Directory) error { - htmlPath := path.Join(dir.DstPath, "index.html") - html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, regularFile) - if err != nil { - log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err) +// Populates a HTMLPayload structure to generate an html listing file, +// propagating the generation recursively. +func writeHTML(dir Directory) (err error) { + var ( + index, relUrl string + html *os.File + ) + + index = path.Join(dir.DstPath, "index.html") + if html, err = os.OpenFile(index, os.O_RDWR|os.O_CREATE, regularFile); err != nil { + return fmt.Errorf("Could not create output file %s:\n%s\n", index, err) } defer html.Close() - out := new(bytes.Buffer) - generateHeader(dir, out) - generateBackLine(dir, out) - generateDirectories(dir.Directories, out) - generateFiles(dir.Files, out) - gen(footer, Footer{Date: time.Now()}, out) + buf := new(bytes.Buffer) + payload := HTMLPayload{ + Root: dir, + Stylesheet: template.CSS(style), + Today: time.Now(), + } - err = minifier.Minify("text/html", html, out) - if err != nil { - log.Fatalf("Could not write to index.html: %s\n%s\n", htmlPath, err) + for _, part := range filepath.SplitList(dir.Path) { + relUrl = path.Join(relUrl, part) + payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) } - err = html.Close() - if err != nil { - log.Fatalf("Could not write to close index.html: %s\n%s\n", htmlPath, err) + if err := page.Execute(buf, payload); err != nil { + return fmt.Errorf("Could not generate listing template:\n%s", err) + } + + if err = minifier.Minify("text/html", html, buf); err != nil { + return fmt.Errorf("Could not minify page output:\n%s", err) } - log.Printf("Generated data for directory: %s\n", dir.Name) return nil } @@ -501,7 +422,7 @@ func logState() { log.Println("\tInclude:\t", includeRegEx.String()) log.Println("\tExclude:\t", excludeRegEx.String()) log.Println("\tRecursive:\t", isRecursive) - log.Println("\tEmpty:\t\t", isEmpty) + log.Println("\tEmpty:\t\t", includeEmpty) log.Println("\tConvert links:\t", convertLink) log.Println("\tSource:\t\t", srcDir) log.Println("\tDstination:\t", dstDir) @@ -537,20 +458,18 @@ func main() { includeRegExStr := flag.String("i", ".*", "A regex pattern to include files into the listing") excludeRegExStr := flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing") _isRecursive := flag.Bool("r", true, "Recursively scan the file tree") - _isEmpty := flag.Bool("empty", false, "Whether to list empty directories") + _includeEmpty := flag.Bool("empty", false, "Whether to list empty directories") _enableSort := flag.Bool("sort", true, "Sort files A-z and by type") rawURL := flag.String("b", "http://localhost", "The base URL") _convertLink := flag.Bool("l", false, "Convert .link files to anchor tags") + pageTemplatePath := flag.String("page", "", "Use a custom listing page template") styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") - headerTemplatePath := flag.String("header", "", "Use a custom header template") - lineTemplatePath := flag.String("line", "", "Use a custom line template") - footerTemplatePath := flag.String("footer", "", "Use a custom footer template") flag.Parse() srcDir = defaultSrc dstDir = defaultDst isRecursive = *_isRecursive - isEmpty = *_isEmpty + includeEmpty = *_includeEmpty enableSort = *_enableSort convertLink = *_convertLink @@ -601,14 +520,8 @@ func main() { minifier.AddFunc("text/html", html.Minify) minifier.AddFunc("application/javascript", js.Minify) - if header, err = loadTemplate("header", *headerTemplatePath, &headerTemplate); err != nil { - log.Fatal("Could not parse header template", err) - } - if line, err = loadTemplate("line", *lineTemplatePath, &lineTemplate); err != nil { - log.Fatal("Could not parse line template", err) - } - if footer, err = loadTemplate("footer", *footerTemplatePath, &footerTemplate); err != nil { - log.Fatal("Could not parse footer template", err) + if page, err = loadTemplate("page", *pageTemplatePath, &pageTemplate); err != nil { + log.Fatal("Could not parse listing page template", err) } if err = readIfNotEmpty(*styleTemplatePath, &style); err != nil { log.Fatal("Could not read stylesheet file", err) @@ -626,6 +539,6 @@ func main() { log.Fatalf("Error while generating JSON metadata:\n%s\n", err) } if err = writeHTML(dir); err != nil { - log.Fatalf("Error while generating JSON metadata:\n%s\n", err) + log.Fatalf("Error while generating HTML page listing:\n%s\n", err) } } From c51c5c5742e0a11330c1d05b97224a94c9ce2d09 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 16:21:34 +0200 Subject: [PATCH 11/16] :fire: --- footer.gohtml | 5 ----- header.gohtml | 13 ------------- line.gohtml | 9 --------- 3 files changed, 27 deletions(-) delete mode 100644 footer.gohtml delete mode 100644 header.gohtml delete mode 100644 line.gohtml diff --git a/footer.gohtml b/footer.gohtml deleted file mode 100644 index f12c378..0000000 --- a/footer.gohtml +++ /dev/null @@ -1,5 +0,0 @@ - -
-

Generated by statik on {{ .Date.Format "02 Jan 06 15:04 MST" }}

- - diff --git a/header.gohtml b/header.gohtml deleted file mode 100644 index a14d568..0000000 --- a/header.gohtml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Index of {{ .FullPath }} - - -

- Index of {{ if not (eq (len .Root.Name) 0) }}/{{ .Root.Name }}{{ end }}/{{ range $i,$a := .Parts }}{{ .Name }}/{{ end }} -

-
-
diff --git a/line.gohtml b/line.gohtml deleted file mode 100644 index 9bd758e..0000000 --- a/line.gohtml +++ /dev/null @@ -1,9 +0,0 @@ -{{ if and .IsDir (eq .Name "..") }} - {{ .Name }} -

{{ .Date.Format "02 Jan 06 15:04 MST" }}

-

{{ .Size }}

-{{ else }} - {{ .Name }} -

{{ .Date.Format "02 Jan 06 15:04 MST" }}

-

{{ .Size }}

-{{ end }} From 5bd4b778f9be412d729890e2a4073ebdcf055cee Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 18 Jun 2022 21:57:30 +0200 Subject: [PATCH 12/16] fix recursion and attempt to fix heading --- statik.go | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/statik.go b/statik.go index a148096..a83d7d1 100644 --- a/statik.go +++ b/statik.go @@ -214,6 +214,7 @@ func newFile(info os.FileInfo, dir string) (fz FuzzyFile, f File, err error) { size = humanize.Bytes(0) name = name[:len(name)-len(linkSuffix)] + rel = rel[:len(rel)-len(linkSuffix)] mime = linkMIME } else if mime, err = mimetype.DetectFile(abs); err != nil { return @@ -283,8 +284,16 @@ func walk(base string) (dir Directory, fz []FuzzyFile, err error) { return } + // Avoid having the root named "." for estetich purpuses: + // Extract an interesting name from the baseURL + name := dirInfo.Name() + if rel == "." && len(baseURL.Path) > 1 { + parts := strings.Split(baseURL.Path, string(os.PathSeparator)) + name = parts[len(parts)-1] + } + dir = Directory{ - Name: dirInfo.Name(), + Name: name, SrcPath: base, DstPath: path.Join(dstDir, rel), URL: withBaseURL(rel), @@ -332,11 +341,12 @@ func copy(f FuzzyFile) (err error) { func writeCopies(dir Directory, fz []FuzzyFile) (err error) { dirs := append([]Directory{dir}, dir.Directories...) - for _, d := range dirs { - dirs = append(dirs, d.Directories...) - if err = os.MkdirAll(d.DstPath, d.Mode); err != nil { - return fmt.Errorf("Could not create output directory %s:\n%s", d.DstPath, err) + for len(dirs) != 0 { + dirs = append(dirs, dirs[0].Directories...) + if err = os.MkdirAll(dirs[0].DstPath, dirs[0].Mode); err != nil { + return fmt.Errorf("Could not create output directory %s:\n%s", dirs[0].DstPath, err) } + dirs = dirs[1:] } for _, f := range fz { if f.MIME == linkMIME { @@ -385,6 +395,12 @@ func writeJSON(dir Directory, fz []FuzzyFile) (err error) { // Populates a HTMLPayload structure to generate an html listing file, // propagating the generation recursively. func writeHTML(dir Directory) (err error) { + for _, d := range dir.Directories { + if err = writeHTML(d); err != nil { + return err + } + } + var ( index, relUrl string html *os.File @@ -403,9 +419,11 @@ func writeHTML(dir Directory) (err error) { Today: time.Now(), } - for _, part := range filepath.SplitList(dir.Path) { - relUrl = path.Join(relUrl, part) - payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) + if dir.Path != "." { + for _, part := range strings.Split(dir.Path, string(os.PathSeparator)) { + relUrl = path.Join(relUrl, part) + payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) + } } if err := page.Execute(buf, payload); err != nil { return fmt.Errorf("Could not generate listing template:\n%s", err) @@ -475,7 +493,7 @@ func main() { args := flag.Args() if len(args) < 1 { - fmt.Printf("Usage: %s [dst] or [src] [dst]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [dst] or [src] [dst]\n", os.Args[0]) os.Exit(1) } if len(args) == 1 { From 57091e7a0f9e6627b2cce9106745a1d5b32745c3 Mon Sep 17 00:00:00 2001 From: Angelo 'Flecart' Huang Date: Sun, 19 Jun 2022 00:05:17 +0200 Subject: [PATCH 13/16] fix(template): fix template broken links and add .. directory --- page.gohtml | 6 +++--- statik.go | 45 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/page.gohtml b/page.gohtml index a63f962..54d4a40 100644 --- a/page.gohtml +++ b/page.gohtml @@ -7,17 +7,17 @@

- Index of {{ if not (eq .Root.Path ".") }}/{{ .Root.Name }}{{ end }}/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }} + Index of {{ if not (eq .Root.Path ".") }}/{{ .Root.Name }}{{ end }}/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }}


{{ range $i,$d := .Root.Directories }} - {{ $d.Name }} + {{ $d.Name }}

{{ $d.ModTime.Format "02 Jan 06 15:04 MST" }}

{{ $d.Size }}

{{ end }} {{ range $i,$f := .Root.Files }} - {{ $f.Name }} + {{ $f.Name }}

{{ $f.ModTime.Format "02 Jan 06 15:04 MST" }}

{{ $f.Size }}

{{ end }} diff --git a/statik.go b/statik.go index a83d7d1..3bd6a10 100644 --- a/statik.go +++ b/statik.go @@ -420,11 +420,19 @@ func writeHTML(dir Directory) (err error) { } if dir.Path != "." { - for _, part := range strings.Split(dir.Path, string(os.PathSeparator)) { + parts := strings.Split(dir.Path, string(os.PathSeparator)) + for _, part := range parts { relUrl = path.Join(relUrl, part) payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) } + oldDirPath := strings.Join(parts[:len(parts)-1], string(os.PathListSeparator)) + oldDir := Directory{ + Name: "..", + URL: withBaseURL(oldDirPath), + } + payload.Root.Directories = append([]Directory{oldDir}, payload.Root.Directories...) } + if err := page.Execute(buf, payload); err != nil { return fmt.Errorf("Could not generate listing template:\n%s", err) } @@ -482,6 +490,8 @@ func main() { _convertLink := flag.Bool("l", false, "Convert .link files to anchor tags") pageTemplatePath := flag.String("page", "", "Use a custom listing page template") styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") + targetHTML := flag.Bool("html", true, "Set false not to build html files (default true)") + targetJSON := flag.Bool("json", true, "Set false not to build JSON metadata (default true)") flag.Parse() srcDir = defaultSrc @@ -545,18 +555,31 @@ func main() { log.Fatal("Could not read stylesheet file", err) } - dir, fz, err := walk(srcDir) - if err != nil { - log.Fatalf("Error when creating the directory structure:\n%s\n", err) - } - if err = writeCopies(dir, fz); err != nil { - log.Fatalf("Error while copying included files to the destination:\n%s\n", err) + var ( + dir Directory + fz []FuzzyFile + ) + if *targetHTML || *targetJSON { + dir, fz, err = walk(srcDir) + if err != nil { + log.Fatalf("Error when creating the directory structure:\n%s\n", err) + } + + if err = writeCopies(dir, fz); err != nil { + log.Fatalf("Error while copying included files to the destination:\n%s\n", err) + } } - if err = writeJSON(dir, fz); err != nil { - log.Fatalf("Error while generating JSON metadata:\n%s\n", err) + if *targetJSON { + if err = writeJSON(dir, fz); err != nil { + log.Fatalf("Error while generating JSON metadata:\n%s\n", err) + } } - if err = writeHTML(dir); err != nil { - log.Fatalf("Error while generating HTML page listing:\n%s\n", err) + + if *targetHTML { + if err = writeHTML(dir); err != nil { + log.Fatalf("Error while generating HTML page listing:\n%s\n", err) + } + } } From 6ed5672dae967990a120a51f01d70ebecba4a956 Mon Sep 17 00:00:00 2001 From: Angelo Huang Date: Sun, 19 Jun 2022 12:08:53 +0200 Subject: [PATCH 14/16] fix(template): basename link problem fixed basename link problem appending to Parts fixed: default true for the flags --- page.gohtml | 6 +++--- statik.go | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/page.gohtml b/page.gohtml index 54d4a40..e40e4b5 100644 --- a/page.gohtml +++ b/page.gohtml @@ -7,17 +7,17 @@

- Index of {{ if not (eq .Root.Path ".") }}/{{ .Root.Name }}{{ end }}/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }} + Index of csunibo/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }}


{{ range $i,$d := .Root.Directories }} - {{ $d.Name }} + {{ $d.Name }}

{{ $d.ModTime.Format "02 Jan 06 15:04 MST" }}

{{ $d.Size }}

{{ end }} {{ range $i,$f := .Root.Files }} - {{ $f.Name }} + {{ $f.Name }}

{{ $f.ModTime.Format "02 Jan 06 15:04 MST" }}

{{ $f.Size }}

{{ end }} diff --git a/statik.go b/statik.go index 3bd6a10..8275a43 100644 --- a/statik.go +++ b/statik.go @@ -419,15 +419,21 @@ func writeHTML(dir Directory) (err error) { Today: time.Now(), } + // always append basePath + payload.Parts = append(payload.Parts, Directory{ + Name: strings.TrimPrefix(strings.TrimSuffix(baseURL.Path, "/"), "/"), + URL: baseURL, + }) if dir.Path != "." { parts := strings.Split(dir.Path, string(os.PathSeparator)) for _, part := range parts { relUrl = path.Join(relUrl, part) payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) } - oldDirPath := strings.Join(parts[:len(parts)-1], string(os.PathListSeparator)) + oldDirPath := path.Join(dir.Path, "..") oldDir := Directory{ Name: "..", + Path: oldDirPath, URL: withBaseURL(oldDirPath), } payload.Root.Directories = append([]Directory{oldDir}, payload.Root.Directories...) @@ -490,8 +496,8 @@ func main() { _convertLink := flag.Bool("l", false, "Convert .link files to anchor tags") pageTemplatePath := flag.String("page", "", "Use a custom listing page template") styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") - targetHTML := flag.Bool("html", true, "Set false not to build html files (default true)") - targetJSON := flag.Bool("json", true, "Set false not to build JSON metadata (default true)") + targetHTML := flag.Bool("html", true, "Set false not to build html files") + targetJSON := flag.Bool("json", true, "Set false not to build JSON metadata") flag.Parse() srcDir = defaultSrc @@ -503,21 +509,23 @@ func main() { args := flag.Args() if len(args) < 1 { - fmt.Fprintf(os.Stderr, "Usage: %s [dst] or [src] [dst]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [-flags] [dst] or [src] [dst]\n", os.Args[0]) os.Exit(1) - } - if len(args) == 1 { + } else if len(args) == 1 { dstDir = args[0] } else if len(args) == 2 { srcDir = args[0] dstDir = args[1] + } else { + fmt.Fprintln(os.Stderr, "Invalid number of arguments, max 2 accepted") + fmt.Fprintf(os.Stderr, "Usage: %s [-flags] [dst] or [src] [dst]\n", os.Args[0]) + os.Exit(1) } if workDir, err = os.Getwd(); err != nil { log.Fatal("Could not get working directory", err) } - // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose srcDir = getAbsPath(srcDir) dstDir = getAbsPath(dstDir) if err = sanitizeDirectories(); err != nil { @@ -534,6 +542,7 @@ func main() { if baseURL, err = url.Parse(*rawURL); err != nil { log.Fatal("Could not parse base URL", err) } + // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose logState() // Ugly hack to generate our custom mime, there currently is no way around this From 6a3973a196b007fa4a7b6ff52e24bc5d50b7d670 Mon Sep 17 00:00:00 2001 From: Angelo Huang Date: Sun, 19 Jun 2022 14:27:32 +0200 Subject: [PATCH 15/16] fix(speed): use pointers for writeJSON|HTML & add quiet flag The speed up factor for this change is 1% --- statik.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/statik.go b/statik.go index 8275a43..b1e6e14 100644 --- a/statik.go +++ b/statik.go @@ -370,7 +370,7 @@ func jsonToFile[T any](path string, v T) (err error) { return nil } -func writeJSON(dir Directory, fz []FuzzyFile) (err error) { +func writeJSON(dir *Directory, fz []FuzzyFile) (err error) { // Write the fuzzy.json file in the root directory if len(fz) != 0 { if err = jsonToFile(path.Join(dir.DstPath, fuzzyFileName), fz); err != nil { @@ -384,7 +384,7 @@ func writeJSON(dir Directory, fz []FuzzyFile) (err error) { } for _, d := range dir.Directories { - if err = writeJSON(d, []FuzzyFile{}); err != nil { + if err = writeJSON(&d, []FuzzyFile{}); err != nil { return } } @@ -394,9 +394,9 @@ func writeJSON(dir Directory, fz []FuzzyFile) (err error) { // Populates a HTMLPayload structure to generate an html listing file, // propagating the generation recursively. -func writeHTML(dir Directory) (err error) { +func writeHTML(dir *Directory) (err error) { for _, d := range dir.Directories { - if err = writeHTML(d); err != nil { + if err = writeHTML(&d); err != nil { return err } } @@ -414,7 +414,7 @@ func writeHTML(dir Directory) (err error) { buf := new(bytes.Buffer) payload := HTMLPayload{ - Root: dir, + Root: *dir, Stylesheet: template.CSS(style), Today: time.Now(), } @@ -498,6 +498,7 @@ func main() { styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") targetHTML := flag.Bool("html", true, "Set false not to build html files") targetJSON := flag.Bool("json", true, "Set false not to build JSON metadata") + enableQuietMode := flag.Bool("q", false, "Set quiet mode: don't print program-state") flag.Parse() srcDir = defaultSrc @@ -542,8 +543,9 @@ func main() { if baseURL, err = url.Parse(*rawURL); err != nil { log.Fatal("Could not parse base URL", err) } - // NOTA: in seguito queste funzioni di logging si possono mettere in if con una flag per verbose - logState() + if !*enableQuietMode { + logState() + } // Ugly hack to generate our custom mime, there currently is no way around this { @@ -580,15 +582,14 @@ func main() { } if *targetJSON { - if err = writeJSON(dir, fz); err != nil { + if err = writeJSON(&dir, fz); err != nil { log.Fatalf("Error while generating JSON metadata:\n%s\n", err) } } if *targetHTML { - if err = writeHTML(dir); err != nil { + if err = writeHTML(&dir); err != nil { log.Fatalf("Error while generating HTML page listing:\n%s\n", err) } - } } From b3675e178b253054d4d840737bfc898d5e21c386 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 20 Jun 2022 09:43:52 +0200 Subject: [PATCH 16/16] final touchups, fix template --- page.gohtml | 2 +- statik.go | 45 ++++++++++++++++++++++----------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/page.gohtml b/page.gohtml index e40e4b5..5d6db23 100644 --- a/page.gohtml +++ b/page.gohtml @@ -7,7 +7,7 @@

- Index of csunibo/{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }} + Index of /{{ range $i,$p := .Parts }}{{ $p.Name }}/{{ end }}


diff --git a/statik.go b/statik.go index b1e6e14..d81f840 100644 --- a/statik.go +++ b/statik.go @@ -419,10 +419,10 @@ func writeHTML(dir *Directory) (err error) { Today: time.Now(), } - // always append basePath + // Always append the last segment of the baseURL as a link back to the home payload.Parts = append(payload.Parts, Directory{ - Name: strings.TrimPrefix(strings.TrimSuffix(baseURL.Path, "/"), "/"), - URL: baseURL, + Name: path.Base(baseURL.Path), + URL: baseURL, }) if dir.Path != "." { parts := strings.Split(dir.Path, string(os.PathSeparator)) @@ -430,13 +430,13 @@ func writeHTML(dir *Directory) (err error) { relUrl = path.Join(relUrl, part) payload.Parts = append(payload.Parts, Directory{Name: part, URL: withBaseURL(relUrl)}) } - oldDirPath := path.Join(dir.Path, "..") - oldDir := Directory{ + + back := path.Join(dir.Path, "..") + payload.Root.Directories = append([]Directory{{ Name: "..", - Path: oldDirPath, - URL: withBaseURL(oldDirPath), - } - payload.Root.Directories = append([]Directory{oldDir}, payload.Root.Directories...) + Path: back, + URL: withBaseURL(back), + }}, payload.Root.Directories...) } if err := page.Execute(buf, payload); err != nil { @@ -450,15 +450,6 @@ func writeHTML(dir *Directory) (err error) { } func logState() { - log.Println("Running with parameters:") - log.Println("\tInclude:\t", includeRegEx.String()) - log.Println("\tExclude:\t", excludeRegEx.String()) - log.Println("\tRecursive:\t", isRecursive) - log.Println("\tEmpty:\t\t", includeEmpty) - log.Println("\tConvert links:\t", convertLink) - log.Println("\tSource:\t\t", srcDir) - log.Println("\tDstination:\t", dstDir) - log.Println("\tBase URL:\t", baseURL.String()) } func sanitizeDirectories() (err error) { @@ -498,7 +489,7 @@ func main() { styleTemplatePath := flag.String("style", "", "Use a custom stylesheet file") targetHTML := flag.Bool("html", true, "Set false not to build html files") targetJSON := flag.Bool("json", true, "Set false not to build JSON metadata") - enableQuietMode := flag.Bool("q", false, "Set quiet mode: don't print program-state") + debug := flag.Bool("d", false, "Print debug logs") flag.Parse() srcDir = defaultSrc @@ -510,7 +501,7 @@ func main() { args := flag.Args() if len(args) < 1 { - fmt.Fprintf(os.Stderr, "Usage: %s [-flags] [dst] or [src] [dst]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [dst] or [src] [dst]\n", os.Args[0]) os.Exit(1) } else if len(args) == 1 { dstDir = args[0] @@ -543,8 +534,16 @@ func main() { if baseURL, err = url.Parse(*rawURL); err != nil { log.Fatal("Could not parse base URL", err) } - if !*enableQuietMode { - logState() + if *debug { + log.Println("Running with parameters:") + log.Println("\tInclude:\t", includeRegEx.String()) + log.Println("\tExclude:\t", excludeRegEx.String()) + log.Println("\tRecursive:\t", isRecursive) + log.Println("\tEmpty:\t\t", includeEmpty) + log.Println("\tConvert links:\t", convertLink) + log.Println("\tSource:\t\t", srcDir) + log.Println("\tDstination:\t", dstDir) + log.Println("\tBase URL:\t", baseURL.String()) } // Ugly hack to generate our custom mime, there currently is no way around this @@ -573,7 +572,7 @@ func main() { if *targetHTML || *targetJSON { dir, fz, err = walk(srcDir) if err != nil { - log.Fatalf("Error when creating the directory structure:\n%s\n", err) + log.Fatalf("Error while walking the filesystem:\n%s\n", err) } if err = writeCopies(dir, fz); err != nil {