cellarspoon
3 years ago
committed by
decentral1se
commit
1190775e94
6 changed files with 748 additions and 0 deletions
@ -0,0 +1 @@ |
|||
test |
@ -0,0 +1,41 @@ |
|||
# distribusi-go |
|||
|
|||
> This is still very experimental, please take a backup of your archives if you |
|||
> are running it on files you care about. It hasn't been tested on large |
|||
> archives. It may still thrash files. Please [report issues] as you find them |
|||
> :100: |
|||
|
|||
A [Go] implementation of [distribusi]. |
|||
|
|||
This is a compiled `distribusi` which is simpler to install on your computer, |
|||
just download the binary, `chmod +x` and run it. |
|||
|
|||
The command-line interface is quite different from the Python version. There |
|||
are less optional flags and more defaults. I shuffled a number of things around |
|||
according to my preferences. For example, I always like my images to be |
|||
thumbnail'd. There's a handy web server built-in now, just run with `-s` |
|||
:metal: |
|||
|
|||
There is no need to install [Pillow] for handling images, that is now built-in. |
|||
The only external dependency is [exiftool] for image captions from embedded |
|||
metadata. If you don't have it `exiftool` installed, then it gracefully skips |
|||
that feature. So, you don't need to install anything else to run `distribusi` |
|||
now :pray: |
|||
|
|||
## Install |
|||
|
|||
``` |
|||
curl https://git.vvvvvvaria.org/decentral1se/distribusi-go/raw/branch/main/distribusi -o distribusi |
|||
chmod +x distribusi |
|||
./distribusi |
|||
``` |
|||
|
|||
## Hacking |
|||
|
|||
You'll need [Go] >= 1.13 installed. Run `go build .` to build a new `./distribusi` executable. |
|||
|
|||
[Go]: https://go.dev |
|||
[distribusi]: https://git.vvvvvvaria.org/varia/distribusi |
|||
[Pillow]: https://pillow.readthedocs.io/en/stable/installation.html#external-libraries |
|||
[exiftool]: https://exiftool.org/ |
|||
[report issues]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues |
Binary file not shown.
@ -0,0 +1,640 @@ |
|||
// Package main is the command-line entrypoint for the distribusi command.
|
|||
package main |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/base64" |
|||
"fmt" |
|||
"image/png" |
|||
"io/fs" |
|||
"io/ioutil" |
|||
"net/http" |
|||
"os" |
|||
"os/exec" |
|||
"path" |
|||
"path/filepath" |
|||
"sort" |
|||
"strings" |
|||
|
|||
logrusStack "github.com/Gurpartap/logrus-stack" |
|||
"github.com/barasher/go-exiftool" |
|||
"github.com/disintegration/imaging" |
|||
"github.com/gabriel-vasile/mimetype" |
|||
"github.com/sirupsen/logrus" |
|||
"github.com/urfave/cli/v2" |
|||
) |
|||
|
|||
// exiftooInstalled tells us if the exiftool binary is installed or not.
|
|||
var exiftoolInstalled = true |
|||
|
|||
// generatedInDistribusi is an internal marker to help recognise when
|
|||
// distribusi-go has generated files.
|
|||
var generatedInDistribusi = "<meta name='generator' content='distribusi-go' />" |
|||
|
|||
// htmlBody is the template for the index.html files that are generated by distribusi-go.
|
|||
var htmlBody = ` |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<!-- Generated with distribusi-go https://git.vvvvvvaria.org/decentral1se/distribusi-go -->
|
|||
%s |
|||
|
|||
<meta http-equiv="content-type" content="text/html; charset=utf-8"> |
|||
|
|||
<style> |
|||
.image { |
|||
max-width: 100%%; |
|||
} |
|||
|
|||
.pdf { |
|||
width: 640px; |
|||
height: 640px; |
|||
} |
|||
|
|||
.os::before { |
|||
content: "📁 "; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
.filename { |
|||
display: block; |
|||
font-family: mono; |
|||
} |
|||
|
|||
.gif { |
|||
width: 450px; |
|||
max-height: 450px; |
|||
} |
|||
|
|||
div { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
margin: 1em; |
|||
padding: 1em; |
|||
} |
|||
|
|||
video { |
|||
width: 450px; |
|||
max-height: 450px; |
|||
} |
|||
|
|||
%s |
|||
</style> |
|||
</head> |
|||
<body> |
|||
%s |
|||
</body> |
|||
</html |
|||
` |
|||
|
|||
// main is the command-line entrypoint.
|
|||
func main() { |
|||
app := &cli.App{ |
|||
Name: "distribusi-go", |
|||
Version: "0.1.0-alpha", |
|||
Usage: "low-tech content management system for the web", |
|||
Description: ` |
|||
Distribusi is a content management system for the web that produces static |
|||
index pages based on folders in the files system. It is inspired by the |
|||
automatic index functions featured in several popular web servers. Distribusi |
|||
works by traversing the file system and directory hierarchy to automatically |
|||
list all the files in the directory, detect the file types and providing them |
|||
with relevant html classes and tags for easy styling. |
|||
|
|||
Example: |
|||
|
|||
distribusi -p myarchive -c styles.css -i .git -s |
|||
`, |
|||
Flags: []cli.Flag{ |
|||
&cli.StringFlag{ |
|||
Name: "css", |
|||
Aliases: []string{"c"}, |
|||
Usage: "css file for custom styles", |
|||
Value: "", |
|||
}, |
|||
&cli.BoolFlag{ |
|||
Name: "debug", |
|||
Aliases: []string{"d"}, |
|||
Usage: "debug logging", |
|||
Value: false, |
|||
}, |
|||
&cli.BoolFlag{ |
|||
Name: "hidden", |
|||
Aliases: []string{"n"}, |
|||
Usage: "include hidden directories (e.g. \".git\")", |
|||
Value: false, |
|||
}, |
|||
&cli.StringFlag{ |
|||
Name: "ignore", |
|||
Aliases: []string{"i"}, |
|||
Usage: "ignore specific paths (e.g. \"mydir, myfile.txt\" )", |
|||
Value: "", |
|||
}, |
|||
&cli.StringFlag{ |
|||
Name: "path", |
|||
Aliases: []string{"p"}, |
|||
Usage: "path to distribusify", |
|||
Required: true, |
|||
}, |
|||
&cli.BoolFlag{ |
|||
Name: "serve", |
|||
Aliases: []string{"s"}, |
|||
Usage: "serve distribusi for the web", |
|||
Value: false, |
|||
}, |
|||
&cli.BoolFlag{ |
|||
Name: "wipe", |
|||
Aliases: []string{"w"}, |
|||
Usage: "remove all generated files", |
|||
Value: false, |
|||
}, |
|||
}, |
|||
Authors: []*cli.Author{ |
|||
{ |
|||
Name: "decentral1se", |
|||
Email: "cellarspoon@riseup.net", |
|||
}, |
|||
}, |
|||
Before: func(c *cli.Context) error { |
|||
if c.Bool("debug") { |
|||
logrus.SetLevel(logrus.DebugLevel) |
|||
logrus.SetFormatter(&logrus.TextFormatter{}) |
|||
logrus.SetOutput(os.Stderr) |
|||
logrus.AddHook(logrusStack.StandardHook()) |
|||
} |
|||
|
|||
return nil |
|||
}, |
|||
Action: func(c *cli.Context) error { |
|||
root, err := filepath.Abs(c.String("path")) |
|||
if err != nil { |
|||
logrus.Fatal(err) |
|||
} |
|||
|
|||
var ignore []string |
|||
if c.String("ignore") != "" { |
|||
ignore = strings.Split(c.String("ignore"), ",") |
|||
|
|||
for i := range ignore { |
|||
ignore[i] = strings.TrimSpace(ignore[i]) |
|||
} |
|||
|
|||
logrus.Debugf("parsed %s as ignore patterns", strings.Join(ignore, " ")) |
|||
} |
|||
|
|||
if c.Bool("wipe") { |
|||
if err := wipeGeneratedFiles(root); err != nil { |
|||
logrus.Fatal(err) |
|||
} |
|||
|
|||
logrus.Infof("wiped all generated files in %s", root) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
_, err = exec.LookPath("exiftool") |
|||
if err != nil { |
|||
logrus.Warn("exiftool is not installed, skipping image captions") |
|||
exiftoolInstalled = false |
|||
} |
|||
|
|||
logrus.Debugf("selecting %s as distribusi root", root) |
|||
|
|||
if err := distribusify(c, root, ignore); err != nil { |
|||
logrus.Fatal(err) |
|||
} |
|||
|
|||
if c.Bool("serve") { |
|||
if err := serveHTTP(root); err != nil { |
|||
logrus.Fatal(err) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}, |
|||
} |
|||
|
|||
sort.Sort(cli.FlagsByName(app.Flags)) |
|||
sort.Sort(cli.CommandsByName(app.Commands)) |
|||
|
|||
if err := app.Run(os.Args); err != nil { |
|||
logrus.Fatal(err) |
|||
} |
|||
} |
|||
|
|||
// distribusify runs the main distribusi generation logic.
|
|||
func distribusify(c *cli.Context, root string, ignore []string) error { |
|||
var allDirs []string |
|||
|
|||
if err := filepath.Walk(root, func(fpath string, finfo os.FileInfo, err error) error { |
|||
|
|||
if skip := shouldSkip(c, fpath, ignore); skip { |
|||
return filepath.SkipDir |
|||
} |
|||
|
|||
var html []string |
|||
|
|||
if finfo.IsDir() { |
|||
var dirs []string |
|||
var files []string |
|||
|
|||
absPath, err := filepath.Abs(fpath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
contents, err := ioutil.ReadDir(absPath) |
|||
if err != nil { |
|||
if strings.Contains(err.Error(), "permission denied") { |
|||
return filepath.SkipDir |
|||
} else { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
for _, content := range contents { |
|||
if content.IsDir() { |
|||
dirs = append(dirs, path.Join(absPath, content.Name())) |
|||
} else { |
|||
if content.Name() == "index.html" { |
|||
file, err := os.ReadFile(path.Join(absPath, content.Name())) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
if strings.Contains(string(file), generatedInDistribusi) { |
|||
continue |
|||
} |
|||
} |
|||
files = append(files, path.Join(absPath, content.Name())) |
|||
} |
|||
} |
|||
|
|||
if len(dirs) == 0 && len(files) == 0 { |
|||
return nil |
|||
} |
|||
|
|||
if root != fpath { |
|||
href := "<a href='../'>../</a>" |
|||
div, err := mkDiv(c, "os/directory", href, "menu", false) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
html = append(html, div) |
|||
} |
|||
|
|||
for _, dir := range dirs { |
|||
fname := filepath.Base(dir) |
|||
|
|||
if skip := shouldSkip(c, dir, ignore); skip { |
|||
continue |
|||
} |
|||
|
|||
mtype := "os/directory" |
|||
|
|||
href := fmt.Sprintf("<a href='%s'>%s/</a>", fname, fname) |
|||
div, err := mkDiv(c, mtype, href, fname, false) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
html = append(html, div) |
|||
} |
|||
|
|||
for _, file := range files { |
|||
fname := filepath.Base(file) |
|||
|
|||
if skip := shouldSkip(c, file, ignore); skip { |
|||
continue |
|||
} |
|||
|
|||
mtype, err := getMtype(file) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
unknown, href, err := getHref(c, file, mtype) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
div, err := mkDiv(c, mtype, href, fname, unknown) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
html = append(html, div) |
|||
} |
|||
|
|||
if err := writeIndex(absPath, html, c.String("css")); err != nil { |
|||
return err |
|||
} |
|||
|
|||
allDirs = append(allDirs, strings.Join(dirs, " ")) |
|||
} |
|||
|
|||
return nil |
|||
}); err != nil { |
|||
return err |
|||
} |
|||
|
|||
logrus.Debugf("generated files in following paths: %s", strings.Join(allDirs, " ")) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// removeIndex safely removes an index.html, making sure to check that
|
|||
// distribusi generated it.
|
|||
func removeIndex(fpath string) error { |
|||
file, err := os.ReadFile(fpath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if strings.Contains(string(file), generatedInDistribusi) { |
|||
if err := os.Remove(fpath); err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// getMtype determines file mimetype which helps to arrange HTML tags
|
|||
// appropriate for the file type.
|
|||
func getMtype(fpath string) (string, error) { |
|||
mtype, err := mimetype.DetectFile(fpath) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
|
|||
return mtype.String(), nil |
|||
} |
|||
|
|||
// genThumb generates an in-memory image thumbnail and encodes it into a base64
|
|||
// representation which is suitable for embedding in HTML pages.
|
|||
func genThumb(c *cli.Context, fpath, caption string) (string, error) { |
|||
knownFailureExts := []string{".ico", ".svg", ".xcf"} |
|||
if sliceContains(knownFailureExts, filepath.Ext(fpath)) { |
|||
return "", nil |
|||
} |
|||
|
|||
imgSrc, err := imaging.Open(fpath, imaging.AutoOrientation(true)) |
|||
if err != nil { |
|||
logrus.Debugf("failed to generate thumbnail for %s", fpath) |
|||
return "", err |
|||
} |
|||
|
|||
img := imaging.Thumbnail(imgSrc, 450, 450, imaging.Lanczos) |
|||
|
|||
buf := new(bytes.Buffer) |
|||
png.Encode(buf, img) |
|||
|
|||
imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes()) |
|||
|
|||
return imgBase64Str, nil |
|||
} |
|||
|
|||
// getCaption retrieves an embedded image caption via exif-tool. If not
|
|||
// exiftool is installed, we gracefully bail out. The caller is responsible for
|
|||
// handling the alternative.
|
|||
func getCaption(c *cli.Context, fpath string) (string, error) { |
|||
var caption string |
|||
|
|||
if !exiftoolInstalled { |
|||
return "", nil |
|||
} |
|||
|
|||
exif, err := exiftool.NewExiftool() |
|||
if err != nil { |
|||
return caption, fmt.Errorf("failed to initialise exiftool, saw %v", err) |
|||
} |
|||
defer exif.Close() |
|||
|
|||
for _, finfo := range exif.ExtractMetadata(fpath) { |
|||
if finfo.Err != nil { |
|||
continue |
|||
} |
|||
|
|||
for k, v := range finfo.Fields { |
|||
if k == "Comment" { |
|||
caption = fmt.Sprintf("%v", v) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return caption, nil |
|||
} |
|||
|
|||
// parseMtype parses a mimetype string to simplify programmatic type lookups.
|
|||
func parseMtype(mtype string) (string, string) { |
|||
stripCharset := strings.Split(mtype, ";") |
|||
splitTypes := strings.Split(stripCharset[0], "/") |
|||
|
|||
ftype, stype := splitTypes[0], splitTypes[1] |
|||
|
|||
return ftype, stype |
|||
} |
|||
|
|||
// sliceContains checks if an element is present in a list.
|
|||
func sliceContains(items []string, target string) bool { |
|||
for _, item := range items { |
|||
if item == target { |
|||
return true |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
// trimFinalNewline trims newlines from the end of bytes just read from files.
|
|||
func trimFinalNewline(contents []byte) string { |
|||
return strings.TrimSuffix(string(contents), "\n") |
|||
} |
|||
|
|||
// getHref figures out which href tag corresponds to which file by navigating
|
|||
// the mimetype. If a type of file is unknown, this is signalled via the bool
|
|||
// return value.
|
|||
func getHref(c *cli.Context, fpath string, mtype string) (bool, string, error) { |
|||
var href string |
|||
var caption string |
|||
var unknown bool |
|||
|
|||
fname := filepath.Base(fpath) |
|||
ftype, stype := parseMtype(mtype) |
|||
|
|||
if ftype == "text" { |
|||
fcontents, err := os.ReadFile(fpath) |
|||
if err != nil { |
|||
return unknown, href, err |
|||
} |
|||
if stype == "html" { |
|||
href = fmt.Sprintf("<section id=\"%s\">%s</section>", fname, trimFinalNewline(fcontents)) |
|||
} else { |
|||
href = fmt.Sprintf("<pre>%s</pre>", trimFinalNewline(fcontents)) |
|||
} |
|||
} else if ftype == "image" { |
|||
caption = "" |
|||
|
|||
exifCaption, err := getCaption(c, fpath) |
|||
if err != nil { |
|||
return unknown, href, nil |
|||
} |
|||
|
|||
if exifCaption != "" { |
|||
caption = exifCaption |
|||
} |
|||
|
|||
if stype == "gif" { |
|||
href = fmt.Sprintf("<img class='gif' src=\"%s\" />", fname) |
|||
} else { |
|||
thumb, err := genThumb(c, fpath, caption) |
|||
if err != nil { |
|||
unknown = true |
|||
href = fmt.Sprintf("<a class='%s' href=\"%s\">%s</a>", stype, fname, fname) |
|||
} else { |
|||
href = fmt.Sprintf( |
|||
"<figure><a href=\"%s\"><img class='thumbnail' src='data:image/jpg;base64,%s'></a><figcaption>%s</figcaption></figure>", |
|||
fname, thumb, caption, |
|||
) |
|||
} |
|||
} |
|||
} else if ftype == "application" { |
|||
if stype == "pdf" { |
|||
href = fmt.Sprintf("<object data=\"%s\" class='pdf' type='application/pdf'><embed src=\"%s\" type='application/pdf'/></object>", fname, fname) |
|||
} else { |
|||
unknown = true |
|||
href = fmt.Sprintf("<a class='%s' href=\"%s\">%s</a>", stype, fname, fname) |
|||
} |
|||
} else if ftype == "audio" { |
|||
href = fmt.Sprintf("<audio controls> <source src=\"%s\" type='audio/%s'> Your browser does not support the audio element. </audio>", fname, stype) |
|||
} else if ftype == "video" { |
|||
href = fmt.Sprintf("<video controls> <source src=\"%s\" type='video/%s'> </video>", fname, stype) |
|||
} else { |
|||
unknown = true |
|||
href = fmt.Sprintf("<a class='%s' href=\"%s\">%s</a>", stype, fname, fname) |
|||
} |
|||
|
|||
return unknown, href, nil |
|||
} |
|||
|
|||
// mkDiv cosntructs a HTML div for inclusion in the generated index.html.
|
|||
func mkDiv(c *cli.Context, mtype string, href, fname string, unknown bool) (string, error) { |
|||
var div string |
|||
|
|||
filename := fmt.Sprintf("<span class='filename'>%s</span>", fname) |
|||
|
|||
ftype, _ := parseMtype(mtype) |
|||
|
|||
if ftype == "text" { |
|||
div = fmt.Sprintf("<div id=\"%s\" class='%s'>%s%s</div>", fname, ftype, href, filename) |
|||
} else if ftype == "os" { |
|||
div = fmt.Sprintf("<div id=\"%s\" class='%s'>%s</div>", fname, ftype, href) |
|||
} else { |
|||
if unknown { |
|||
// we really don't know what this is, so the filename is the href and we avoid adding it again
|
|||
div = fmt.Sprintf("<div id=\"%s\" class='%s'>%s</div>", fname, ftype, href) |
|||
} else { |
|||
// images, videos, etc. still get a filename
|
|||
div = fmt.Sprintf("<div id=\"%s\" class='%s'>%s%s</div>", fname, ftype, href, filename) |
|||
} |
|||
} |
|||
|
|||
return div, nil |
|||
} |
|||
|
|||
// writeIndex writes a new index.html.
|
|||
func writeIndex(fpath string, html []string, styles string) error { |
|||
body := fmt.Sprintf(htmlBody, generatedInDistribusi, "", strings.Join(html, "\n")) |
|||
HTMLPath := path.Join(fpath, "index.html") |
|||
contents := []byte(body) |
|||
|
|||
if styles != "" { |
|||
absPath, err := filepath.Abs(styles) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if _, err := os.Stat(absPath); !os.IsNotExist(err) { |
|||
contents, err := os.ReadFile(absPath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
body = fmt.Sprintf(htmlBody, generatedInDistribusi, contents, strings.Join(html, "\n")) |
|||
} |
|||
} |
|||
|
|||
if _, err := os.Stat(HTMLPath); err != nil { |
|||
if os.IsNotExist(err) { |
|||
if err := ioutil.WriteFile(HTMLPath, contents, 0644); err != nil { |
|||
return err |
|||
} |
|||
} else { |
|||
return err |
|||
} |
|||
} else { |
|||
file, err := os.ReadFile(HTMLPath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if strings.Contains(string(file), generatedInDistribusi) { |
|||
if err := ioutil.WriteFile(HTMLPath, contents, 0644); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// serveHTTP serves a web server for browsing distribusi.
|
|||
func serveHTTP(fpath string) error { |
|||
fs := http.FileServer(http.Dir(fpath)) |
|||
http.Handle("/", fs) |
|||
|
|||
logrus.Info("distribusi live @ http://localhost:3000") |
|||
|
|||
if err := http.ListenAndServe(":3000", nil); err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// wipeGeneratedFiles removes all distribusi generated files.
|
|||
func wipeGeneratedFiles(dir string) error { |
|||
if err := filepath.WalkDir(dir, func(fpath string, dirEntry fs.DirEntry, err error) error { |
|||
fname := filepath.Base(fpath) |
|||
if fname == "index.html" { |
|||
if err := removeIndex(fpath); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
return nil |
|||
}); err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// shouldSkip checks if paths should be skipped over.
|
|||
func shouldSkip(c *cli.Context, fpath string, ignore []string) bool { |
|||
if sliceContains(ignore, filepath.Base(fpath)) { |
|||
return true |
|||
} |
|||
|
|||
fpaths := strings.Split(fpath, "/") |
|||
for _, part := range fpaths { |
|||
if strings.HasPrefix(part, ".") { |
|||
if !c.Bool("hidden") { |
|||
return true |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
@ -0,0 +1,22 @@ |
|||
module varia.zone/distribusi |
|||
|
|||
go 1.17 |
|||
|
|||
require ( |
|||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 |
|||
github.com/barasher/go-exiftool v1.7.0 |
|||
github.com/disintegration/imaging v1.6.2 |
|||
github.com/gabriel-vasile/mimetype v1.4.0 |
|||
github.com/sirupsen/logrus v1.8.1 |
|||
github.com/urfave/cli/v2 v2.3.0 |
|||
) |
|||
|
|||
require ( |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect |
|||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect |
|||
github.com/russross/blackfriday/v2 v2.0.1 // indirect |
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect |
|||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect |
|||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect |
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect |
|||
) |
@ -0,0 +1,44 @@ |
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
|||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8= |
|||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs= |
|||
github.com/barasher/go-exiftool v1.7.0 h1:EOGb5D6TpWXmqsnEjJ0ai6+tIW2gZFwIoS9O/33Nixs= |
|||
github.com/barasher/go-exiftool v1.7.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= |
|||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= |
|||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= |
|||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= |
|||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= |
|||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= |
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= |
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= |
|||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= |
|||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
|||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= |
|||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= |
|||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= |
|||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= |
|||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= |
|||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
|||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= |
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
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= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
Loading…
Reference in new issue