diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..de610b6 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,18 @@ +--- +kind: pipeline +name: coopcloud.tech/abra +steps: + - name: make check + image: golang:1.21 + commands: + - make check + + - name: make test + image: golang:1.21 + commands: + - make test + depends_on: + - make check +volumes: + - name: deps + temp: {} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d2f05b2..ca93c03 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,24 +1,25 @@ --- project_name: distribusi - -gitea_urls: - api: https://git.vvvvvvaria.org/api/v1 - download: https://git.vvvvvvaria.org/ - skip_tls_verify: false +version: 2 before: hooks: - go mod tidy - - go generate ./... + +upx: + - enabled: true + compress: best + lzma: true builds: - - env: + - id: distribusi + binary: distribusi + env: - CGO_ENABLED=0 goos: - - linux - - windows - darwin + - linux goarch: - 386 - amd64 @@ -28,33 +29,3 @@ builds: - 5 - 6 - 7 - ldflags: - - "-X 'main.Version={{ .Version }}'" - - "-X 'main.Commit={{ .Commit }}'" - -archives: - - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 - format: binary - -checksum: - name_template: 'checksums.txt' - -snapshot: - name_template: "{{ incpatch .Version }}-alpha" - -changelog: - sort: asc - filters: - exclude: - - "^Revert" - - "^build:" - - "^docs:" - - "^refactor:" - - "^test:" - - "^tests:" - - "^wip:" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1e189db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +## Hacking + +You'll need [Go] >= 1.11 installed. + +Run `make` to build a new `./distribusi` executable. + +[Go]: https://go.dev + +### Tests + +``` +make test +``` + +### Generating the logo + +``` +cat contrib/logo/LOGO | lolcat -F 0.1 +``` + +### Release + +Currently, you have to be `@decentral1se` to do this: + +```bash +git tag +VERSION= make release +``` + +You can build a local-only release by running this: + +```bash +goreleaser release --snapshot --clean +``` + +Binaries are in `dist`. diff --git a/Makefile b/Makefile index 4db02f2..17d904a 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,36 @@ COMMIT := $(shell git rev-list -1 HEAD) -LDFLAGS := "-X 'main.Commit=$(COMMIT)'" -DIST_LDFLAGS := $(LDFLAGS)" -s -w" +GCFLAGS := -gcflags="all=-l -B" +LDFLAGS := -ldflags="-X 'main.Commit=$(COMMIT)' -s -w" +ALLFLAGS := $(GCFLAGS) $(LDFLAGS) default: build -build: - @go build -ldflags=$(LDFLAGS) . +check: + @test -z $$(gofmt -l .) || \ + (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1) + +format: + @gofmt -s -w . + +build: check + @go build $(ALLFLAGS) . install: - @go install -ldflags=$(LDFLAGS) . + @go install $(ALLFLAGS) . test: @go test ./... -cover -v clean: - @rm -f /tmp/distribusi-go* && rm ./distribusi + @rm ./distribusi + +upx: + @upx --lzma ./distribusi release: - @goreleaser release --snapshot --rm-dist && \ - ssh varia.zone mkdir -p public_html/distribusi-go/$(VERSION) && \ - scp -r dist/* varia.zone:public_html/distribusi-go/$(VERSION) + @goreleaser release --snapshot --clean -.PHONY: build install test clean release +deps: + @go get -t -u ./... + +.PHONY: check format build install test clean upx release deps diff --git a/README.md b/README.md index 77fd0f9..98c73f8 100644 --- a/README.md +++ b/README.md @@ -1,203 +1,22 @@ # distribusi-go +[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/distribusi-go/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/distribusi-go) +[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/distribusi-go)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/distribusi-go) +[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/distribusi-go.svg)](https://pkg.go.dev/coopcloud.tech/distribusi-go) +

-An implementation of [distribusi] in [a faster programming language]. +`distribusi` is a low-tech content management system for the web that produces +static index pages based on folders in the filesystem. It is inspired by the +automatic index functions featured in several web servers. -`distribusi` is a content management system for the web that produces static -index pages based on folders in the filesystem. It is inspired by the automatic -index functions featured in several web servers. It works by traversing the -file system and directory hierarchy to automatically list all the files in the -directory and providing them with html classes and tags for easy styling. - -If you're coming from the Python version, [here are the differences]. - -## Install - -> Current release: **0.1.1-alpha** - -Enterprise :tm: cross-platform support is available :champagne: If your -platform isn't listed below, please see [the entire listing]. If you can't find -it there, we can probably add support for it (see `$GOOS`/`$GOARCH` [on -go.dev]), please [open a ticket]. Package manager installation methods are -planned for Some Time Soon :tm: (e.g. `apt install distribusi`, see [this -ticket] to track). - -### GNU/Linux - -#### GNU/Linux (amd64) - -``` -curl https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_linux_amd64/distribusi -o distribusi -chmod +x distribusi -./distribusi -h # check things work -``` - -#### GNU/Linux (386) - -``` -curl https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_linux_386/distribusi -o distribusi -chmod +x distribusi -./distribusi -h # check things work -``` - -#### GNU/Linux (arm64) - -``` -curl https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_linux_arm64/distribusi -o distribusi -chmod +x distribusi -./distribusi -h # check things work -``` - -### Mac OS X - -#### Mac OS X (amd64) - -``` -curl https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_darwin_amd64/distribusi -o distribusi -chmod +x distribusi -./distribusi -h # check things work -``` - -#### Mac OS X (arm64) - -``` -curl https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_darwin_arm64/distribusi -o distribusi -chmod +x distribusi -./distribusi -h # check things work -``` - -### Windows - -I've no idea how you even install this on Windoze! Maybe you can help by -sending a documentation patch? 🙏 - -#### Windows (amd64) - -> https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_windows_amd64/distribusi.exe - -#### Windows (386) - -> https://vvvvvvaria.org/~decentral1se/distribusi-go/0.1.1-alpha/distribusi_windows_386/distribusi.exe - -## Quick start - -Run `distribusi-go` on `` and serve it locally for viewing: - -```bash -./distribusi -p -s -``` - -If you want to ignore certain paths, you can use `-i/--ignore`: - -```bash -./distribusi -p -i '*.gif, *.md, mydir' -``` - -It supports a list of patterns in a comma separated list, wildcards work, more [on pkg.go.dev]. - -If you run into issues, run with `-d/--debug`. All debug logs are stored in a time stamped log file under `/tmp/`. - -```bash -ls /tmp/ | grep -i distribusi -distribusi-go-20-25-492637114356 -``` - -If you have SSH access to a server, you can publish your files with `-P/--publish`. - -The syntax works just like [scp] and it uses SSH to transfer the files under the hood: - -``` -./distribusi -p -P : -``` - -See [this short guide](#ssh-guide-for-p-publish) for help with SSH connection issues. - -:v: +A re-implementation of [`varia/distribusi`](https://git.vvvvvvaria.org/varia/distribusi) in a faster programming language. ## Hacking -You'll need [Go] >= 1.11 installed. Run `make` to build a new `./distribusi` executable. - -If you wanna learn Go, I've got [this book], feel free to take a loan of it :grinning: - -## Tips - -### SSH guide for `-P/--publish` - -`distribusi-go` only supports one connection method right now: SSH public key -authentication using `ssh-agent`. This is mostly for simplicity, there was a -discussion running [over here] for a while. - -Let's take a practical example. First, make sure your `ssh-agent` is running -and your SSH key is loaded: - -``` -eval $(ssh-agent -s) -ssh-add ~/.ssh/ -ssh-add -L # see loaded keys -``` - -Now, when you type `-P :...`, `distribusi-go` will look up an entry in -your `~/.ssh/config` with a matching `` value. I.e. if you want to run -`./distribusi -p -P varia.zone:/var/www/`, then a matching -`~/.ssh/config` entry might look like this: - -```ssh -Host varia.zone - Hostname varia.zone - User decentral1se - Port 22 - IdentityFile ~/.ssh/ -``` - -`distribusi-go` will read the `User` and `Port` values from this configuration. -It won't try to parse private key files or prompt for passwords, it will simply -interface with `ssh-agent` which handles all that. If there is no -`~/.ssh/config` entry, default values will be attempted. - -If all else fails, try `-d/--debug` for extra help figuring out what SSH -connection details are used. You can [open a ticket] for support also. - -### Generating the logo - -``` -cat contrib/logo/LOGO | lolcat -F 0.1 -``` - -## Differences compared to the Python version - -- It's simpler to install on your computer, just download the binary, `chmod - +x` and run it 🎉 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 `exiftool` installed, then - it gracefully skips that feature. - -- The command-line interface is quite different. There are less optional flags - and more defaults. I shuffled a number of things around according to my - preferences (open to changes, please [open a ticket]). For example, I always - like my images to be thumbnail'd. - -Continue with the [install instructions]. - -## Release - -Currently, you have to be `@decentral1se` to do this: - -```bash -git tag -VERSION= make release -``` - -You can build a local-only release by running this: - -```bash -goreleaser release --snapshot --rm-dist -``` - -Binaries are in `dist`. +[`CONTRIBUTING.md`](./CONTRIBUTING.md) ## Acknowledgements @@ -208,20 +27,3 @@ Binaries are in `dist`. - -[a faster programming language]: https://go.dev -[Pillow]: https://pillow.readthedocs.io/en/stable/installation.html#external-libraries -[distribusi]: https://git.vvvvvvaria.org/varia/distribusi -[exiftool]: https://exiftool.org/ -[here are the differences]: #differences-compared-to-the-python-version -[install instructions]: #install -[Go]: https://go.dev -[on go.dev]: https://go.dev/doc/install/source#environment -[on pkg.go.dev]: https://pkg.go.dev/path/filepath#Match -[open a ticket]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues/new/choose -[over here]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues/4 -[report issues]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues/new/choose -[scp]: https://linux.die.net/man/1/scp -[the entire listing]: https://vvvvvvaria.org/~decentral1se/distribusi-go/ -[this book]: https://www.gopl.io/ -[this ticket]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues/1 diff --git a/distribusi.go b/distribusi.go index bec4951..91cc421 100644 --- a/distribusi.go +++ b/distribusi.go @@ -1,65 +1,42 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -// Package main is the command-line entrypoint for the distribusi command. +// Package main provides the command-line entrypoint. package main import ( - "bytes" - "encoding/base64" + "errors" + "flag" "fmt" - "image/png" - "io" "io/fs" "io/ioutil" - "net" + "log/slog" "net/http" "os" - "os/exec" - "os/user" "path" "path/filepath" - "sort" "strings" "time" - logrusStack "github.com/Gurpartap/logrus-stack" - "github.com/barasher/go-exiftool" - "github.com/disintegration/imaging" "github.com/gabriel-vasile/mimetype" - "github.com/k0kubun/go-ansi" - "github.com/kevinburke/ssh_config" - "github.com/povsister/scp" - "github.com/schollz/progressbar/v3" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" + thumb "github.com/prplecake/go-thumbnail" ) -// Version is the current version of distribusi-go. -var Version string +// logger is the global logger. +var logger *slog.Logger -// Commit is the current commit of distribusi-go. -var Commit string - -// port is the web server port. -var port = ":3000" - -// exiftooInstalled tells us if the exiftool binary is installed or not. -var exiftoolInstalled = true +// thumbnailSuffix is the file name suffix used for generated thumbnails. +var thumbnailSuffix = "_distribusi_thumbnail" +// logStripMsg strips content from logs messages for brevity. var logStripMsg = "...stripped from logs for brevity..." -// generatedInDistribusi is an internal marker to help recognise when -// distribusi-go has generated files. -var generatedInDistribusi = "" +// port is for serving locally +var port = ":1312" // htmlBody is the template for the index.html files that are generated by distribusi-go. var htmlBody = ` - + %s @@ -128,15 +105,13 @@ var htmlTags = map[fileType]map[subType]htmlTag{ "image": { "thumbnail": trimAllNewlines(`
- + -
%s
`), "generic": trimAllNewlines(`
- + -
%s
`), }, "application": { @@ -147,7 +122,7 @@ var htmlTags = map[fileType]map[subType]htmlTag{ "audio": { "generic": trimAllNewlines(``), }, "video": { @@ -160,359 +135,162 @@ var htmlTags = map[fileType]map[subType]htmlTag{ }, } -// main is the command-line entrypoint for the program. +// generatedInDistribusi is an internal marker to help recognise when +// distribusi-go has generated files. +var generatedInDistribusi = "" + +var helpOutput = `USAGE: + distribusi [options] [arguments] + +DESCRIPTION: + A low-tech 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. + +ARGUMENTS: + path to distribusify (default: ".") + +OPTIONS: + -d show debug output + -c css file for custom styles + -h output help + -i ignore paths (e.g. "*.gif") + -s serve locally + -v output version + -w wipe generated files +` + +var ( + cssFlag string + ignoreFlag string + + debugFlag bool + helpFlag bool + serveFlag bool + wipeFlag bool + + version = "0.2.0" + versionFlag bool +) + func main() { - if Version == "" { - Version = "dev" + flag.StringVar(&cssFlag, "c", "", "") + flag.StringVar(&ignoreFlag, "i", "", "") + + flag.BoolVar(&debugFlag, "d", false, "") + flag.BoolVar(&helpFlag, "h", false, "") + flag.BoolVar(&serveFlag, "s", false, "") + flag.BoolVar(&versionFlag, "v", false, "") + flag.BoolVar(&wipeFlag, "w", false, "") + + flag.Usage = func() { + return } - if Commit == "" { - Commit = " " + flag.Parse() + + opts := &slog.HandlerOptions{ + Level: slog.LevelInfo, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == "time" { + t := a.Value.Time() + return slog.String("time", t.Format(time.Kitchen)) + } + return a + }, + } + if debugFlag { + opts.Level = slog.LevelDebug + opts.AddSource = true + } + logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + slog.SetDefault(logger) + + if helpFlag { + fmt.Print(helpOutput) + os.Exit(0) } - app := &cli.App{ - Name: "distribusi-go", - Version: fmt.Sprintf("%s-%s", Version, Commit[:7]), - 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. + if versionFlag { + fmt.Printf("%s\n", version) + os.Exit(0) + } -Example: + path, err := os.Getwd() + if err != nil { + logger.Error(fmt.Sprintf("unable to determine current working directory: %s", err)) + } + if len(flag.Args()) == 1 { + path = flag.Args()[0] + } - distribusi -p -s -`, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "css", - Aliases: []string{"c"}, - Usage: "css file for custom styles", - Value: "", - Required: false, - }, - &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. "*.gif, *.txt, mydir" )`, - Value: "", - Required: false, - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Usage: "path to distribusify", - Required: true, - }, - &cli.StringFlag{ - Name: "publish", - Aliases: []string{"P"}, - Usage: `publish to a server using scp (e.g. "varia.zone:public_html")`, - Required: false, - }, - &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: "Varia & friends", - Email: "info@varia.zone", - }, - }, - Before: func(c *cli.Context) error { - if c.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) - logrus.SetFormatter(&logrus.TextFormatter{}) + root, err := filepath.Abs(path) + if err != nil { + logger.Error(fmt.Sprintf("unable to determine absolute path of %s: %s", path, err)) + } - logFile, err := getLogFile() - if err != nil { - logrus.Fatalf("unable to set up a log file, saw: %s", err) - } + if _, err := os.Stat(root); os.IsNotExist(err) { + logger.Error(fmt.Sprintf("%s does not exist?", root)) + } - logrus.SetOutput(io.MultiWriter(os.Stderr, logFile)) - logrus.RegisterExitHandler(func() { - if logFile == nil { - return - } - logFile.Close() - }) - logrus.AddHook(logrusStack.StandardHook()) - } + logger.Debug(fmt.Sprintf("selecting %s as distribusi root", root)) - return nil - }, - Action: func(c *cli.Context) error { - if c.Bool("serve") && c.String("publish") != "" { - logrus.Fatal("woops, can't publish & serve at the same time?") - } + var ignore []string + if ignoreFlag != "" { + ignore = strings.Split(ignoreFlag, ",") + for i := range ignore { + ignore[i] = strings.TrimSpace(ignore[i]) + } + logger.Debug(fmt.Sprintf("parsed %s as ignore patterns", strings.Join(ignore, " "))) + } - root, err := filepath.Abs(c.String("path")) - if err != nil { - logrus.Fatal(err) - } + if wipeFlag { + if err := wipeGeneratedFiles(root); err != nil { + logger.Error(err.Error()) + } + logger.Info(fmt.Sprintf("wiped generated files in %s", root)) + return + } - if _, err := os.Stat(root); os.IsNotExist(err) { - logrus.Fatalf("%s does not exist?", root) - } + ch := make(chan error, 2) + go func() { + if err := distribusify(root, ignore); err != nil { + ch <- err + return + } - logrus.Debugf("selecting %s as distribusi root", root) + msg := " done!" + if serveFlag { + fmt.Printf(msg) + } else { + fmt.Println(msg) + } - var ignore []string - if c.String("ignore") != "" { - ignore = strings.Split(c.String("ignore"), ",") + ch <- nil + return + }() - 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.Debug("exiftool is not installed, skipping image captions") - exiftoolInstalled = false - } - - ch := make(chan error, 2) - go func() { - if err := distribusify(c, root, ignore); err != nil { - ch <- err - return - } - - if c.Bool("serve") { - fmt.Printf("done!") - } else { - fmt.Println("done!") - } - - pubTarget := c.String("publish") - if pubTarget != "" { - logrus.Debugf("attempting to publish files to %s", pubTarget) - - if err := scpPublish(c, root, pubTarget); err != nil { - ch <- err - return - } - } - - ch <- nil + if serveFlag { + go func() { + logger.Debug("attempting to start up the web server") + if err := serveHTTP(root); err != nil { + ch <- err return - }() - - if c.Bool("serve") { - go func() { - logrus.Debug("attempting to start up the web server") - - if err := serveHTTP(root); err != nil { - ch <- err - return - } - - ch <- nil - return - }() - } else { - // close the channel, we're not serving anything - ch <- nil } - - for i := 1; i <= 2; i++ { - err := <-ch - if err != nil { - logrus.Fatal(err) - } - } - - return nil - }, + ch <- nil + return + }() + } else { + // NOTE(d1): close the channel, we're not serving anything + ch <- 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 - - progress := mkProgressBar(c) - - if err := filepath.Walk(root, func(fpath string, finfo os.FileInfo, err error) error { - - skip, err := shouldSkip(c, fpath, ignore) + for i := 1; i <= 2; i++ { + err := <-ch if err != nil { - return err + logger.Error(err.Error()) } - if skip { - return nil - } - - var html []string - - if finfo.IsDir() { - var dirs []string - var files []string - - absPath, err := filepath.Abs(fpath) - if err != nil { - logrus.Debugf("unable to read %s", absPath) - return nil - } - - contents, err := ioutil.ReadDir(absPath) - if err != nil { - logrus.Debugf("unable to read %s", absPath) - return nil - } - - for _, content := range contents { - if content.IsDir() { - dirs = append(dirs, path.Join(absPath, content.Name())) - } else { - if content.Name() == "index.html" { - indexPath := path.Join(absPath, content.Name()) - file, err := os.ReadFile(indexPath) - if err != nil { - logrus.Debugf("unable to read %s, skipping", content.Name()) - continue - } - - if strings.Contains(string(file), generatedInDistribusi) { - logrus.Debugf("%s was not generated by distribusi, skipping", indexPath) - continue - } - } - - files = append(files, path.Join(absPath, content.Name())) - } - } - - if len(dirs) == 0 && len(files) == 0 { - return nil - } - - if root != fpath { - href := "../" - 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) - - skip, err := shouldSkip(c, fname, ignore) - if err != nil { - return err - } - if skip { - continue - } - - mtype := "os/directory" - - href := fmt.Sprintf("%s/", fname, fname) - div, err := mkDiv(c, mtype, href, fname, false) - if err != nil { - return err - } - - html = append(html, div) - progress.Add(1) - } - - for _, file := range files { - fname := filepath.Base(file) - - skip, err := shouldSkip(c, fname, ignore) - if err != nil { - return err - } - if skip { - continue - } - - mtype, err := getMtype(file) - if err != nil { - logrus.Debugf("failed to read mimetype of %s", file) - continue - } - - unknown, href, err := mkHref(c, file, mtype) - if err != nil { - logrus.Debugf("failed to generate href for %s", file) - continue - } - - div, err := mkDiv(c, mtype, href, fname, unknown) - if err != nil { - logrus.Debugf("failed to generate div for %s", file) - continue - } - - html = append(html, div) - progress.Add(1) - } - - if err := mkIndex(absPath, html, c.String("css")); err != nil { - logrus.Debugf("unable to generated %s, skipping", path.Join(absPath, "index.html")) - return nil - } - - 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 @@ -522,195 +300,44 @@ func removeIndex(fpath string) error { 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 reads file mimetypes directlry from files. -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) { - imgSrc, err := imaging.Open(fpath, imaging.AutoOrientation(true)) - if err != nil { - return "", err - } - - img := imaging.Thumbnail(imgSrc, 450, 450, imaging.Lanczos) - - buf := new(bytes.Buffer) - png.Encode(buf, img) - - imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes()) - - logrus.Debugf("successfully generated thumbnail for %s", fpath) - - return imgBase64Str, nil -} - -// getCaption retrieves an embedded image caption via exif-tool. If exiftool is -// not 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) +// wipeGeneratedFiles removes all distribusi generated files under a file +// system path. We do take care to avoid deleting files that distribusi has not +// generated by checking their contents. +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 fmt.Errorf("unable to remove %s: %s", fpath, err) } - } - } - - if caption != "" { - logrus.Debugf("retrieved caption %s from %s", caption, fpath) - } else { - logrus.Debugf("no comment retrieved for %s", fpath) - } - - return caption, nil -} - -// parseMtype parses a mimetype string to simplify programmatic type lookups. -func parseMtype(mtype string) (string, string, error) { - if !strings.Contains(mtype, "/") { - return "", "", fmt.Errorf("unable to parse %s", mtype) - } - - stripCharset := strings.Split(mtype, ";") - splitTypes := strings.Split(stripCharset[0], "/") - ftype, stype := splitTypes[0], splitTypes[1] - - return ftype, stype, nil -} - -// trimFinalNewline trims newlines from the end of bytes just read from files. -func trimFinalNewline(contents []byte) string { - return strings.TrimSuffix(string(contents), "\n") -} - -// trimAllNewlines removes all new lines. -func trimAllNewlines(contents string) string { - return strings.ReplaceAll(string(contents), "\n", "") -} - -// mkHref 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 mkHref(c *cli.Context, fpath string, mtype string) (bool, string, error) { - var href string - var hrefTemplate string - var strippedDebugOutput string - var unknown bool - - fname := filepath.Base(fpath) - ftype, stype, err := parseMtype(mtype) - if err != nil { - return unknown, href, err - } - - if ftype == "text" { - fcontents, err := os.ReadFile(fpath) - if err != nil { - return unknown, href, err - } - - trimmed := trimFinalNewline(fcontents) - - if stype == "html" { - hrefTemplate = htmlTags[ftype][stype] - href = fmt.Sprintf(hrefTemplate, fname, trimmed) - strippedDebugOutput = fmt.Sprintf(hrefTemplate, fname, logStripMsg) - } else { - hrefTemplate = htmlTags[ftype]["generic"] - href = fmt.Sprintf(hrefTemplate, trimmed) - strippedDebugOutput = fmt.Sprintf(hrefTemplate, logStripMsg) - } - } else if ftype == "image" { - caption, err := getCaption(c, fpath) - if err != nil { - return unknown, href, nil - } - - if stype == "gif" { - hrefTemplate = htmlTags[ftype]["generic"] - href = fmt.Sprintf(hrefTemplate, fname, stype, fname, caption) - } else { - thumb, err := genThumb(c, fpath, caption) - if err != nil { - hrefTemplate = htmlTags[ftype]["generic"] - href = fmt.Sprintf(hrefTemplate, fname, stype, fname, caption) - logrus.Debugf("failed to generate thumbnail for %s, showing original image", fpath) - } else { - hrefTemplate = htmlTags[ftype]["thumbnail"] - href = fmt.Sprintf(hrefTemplate, fname, thumb, caption) - strippedDebugOutput = fmt.Sprintf(hrefTemplate, fname, logStripMsg, caption) + logger.Debug(fmt.Sprintf("wiping %s as requested", fpath)) + } else if strings.Contains(fname, thumbnailSuffix) { + if err := os.Remove(fpath); err != nil { + return fmt.Errorf("unable to remove %s: %s", fpath, err) } + logger.Debug(fmt.Sprintf("wiping %s as requested", fpath)) } - } else if ftype == "application" { - if stype == "pdf" { - hrefTemplate = htmlTags[ftype][stype] - href = fmt.Sprintf(hrefTemplate, fname, fname) - } else { - unknown = true - hrefTemplate = htmlTags["unknown"]["generic"] - href = fmt.Sprintf(hrefTemplate, stype, fname, fname) - } - } else if ftype == "audio" { - hrefTemplate = htmlTags[ftype]["generic"] - href = fmt.Sprintf(hrefTemplate, fname, stype) - } else if ftype == "video" { - hrefTemplate = htmlTags[ftype]["generic"] - href = fmt.Sprintf(hrefTemplate, fname, stype) - } else { - unknown = true - hrefTemplate = htmlTags["unknown"]["generic"] - href = fmt.Sprintf(hrefTemplate, stype, fname, fname) - } - if strippedDebugOutput != "" { - logrus.Debugf("%s was wrapped in: %s", fname, strippedDebugOutput) - } else { - logrus.Debugf("%s was wrapped in: %s", fname, href) + return nil + }); err != nil { + return err } - - return unknown, href, nil + return nil } -// mkDiv cosntructs a HTML div for inclusion in the generated index.html. These +// createDiv cosntructs a HTML div for inclusion in the generated index.html. These // dives are used to wrap the elements that appear on generated pages with // relevant identifiers for convenient styling. -func mkDiv(c *cli.Context, mtype string, href, fname string, unknown bool) (string, error) { +func createDiv(mtype string, href, fname string, unknown bool) (string, error) { var div string var divTemplate string var strippedDebugOutput string @@ -748,16 +375,169 @@ func mkDiv(c *cli.Context, mtype string, href, fname string, unknown bool) (stri } if strippedDebugOutput != "" { - logrus.Debugf("%s was wrapped in: %s", fname, strippedDebugOutput) + logger.Debug(fmt.Sprintf("%s was wrapped in: %s", fname, strippedDebugOutput)) } else { - logrus.Debugf("%s was wrapped in: %s", fname, div) + logger.Debug(fmt.Sprintf("%s was wrapped in: %s", fname, div)) } return div, nil } -// mkIndex writes a new generated index.html file to the file system. -func mkIndex(fpath string, html []string, styles string) error { +// parseMtype parses a mimetype string to simplify programmatic type lookups. +func parseMtype(mtype string) (string, string, error) { + if !strings.Contains(mtype, "/") { + return "", "", fmt.Errorf("unable to parse %s", mtype) + } + + stripCharset := strings.Split(mtype, ";") + splitTypes := strings.Split(stripCharset[0], "/") + ftype, stype := splitTypes[0], splitTypes[1] + + return ftype, stype, nil +} + +// trimAllNewlines removes all new lines. +func trimAllNewlines(contents string) string { + return strings.ReplaceAll(string(contents), "\n", "") +} + +// createHref 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 createHref(fpath string, mtype string) (bool, string, error) { + var ( + href string + hrefTemplate string + strippedDebugOutput string + unknown bool + ) + + fname := filepath.Base(fpath) + ftype, stype, err := parseMtype(mtype) + if err != nil { + return unknown, href, err + } + + if ftype == "text" { + fcontents, err := os.ReadFile(fpath) + if err != nil { + return unknown, href, err + } + + trimmed := strings.TrimSuffix(string(fcontents), "\n") + + if stype == "html" { + hrefTemplate = htmlTags[ftype][stype] + href = fmt.Sprintf(hrefTemplate, fname, trimmed) + strippedDebugOutput = fmt.Sprintf(hrefTemplate, fname, logStripMsg) + } else { + hrefTemplate = htmlTags[ftype]["generic"] + href = fmt.Sprintf(hrefTemplate, trimmed) + strippedDebugOutput = fmt.Sprintf(hrefTemplate, logStripMsg) + } + } else if ftype == "image" { + if stype == "gif" { + hrefTemplate = htmlTags[ftype]["generic"] + href = fmt.Sprintf(hrefTemplate, fname, stype, fname) + } else { + thumbPath, err := genThumb(fpath) + if err != nil { + hrefTemplate = htmlTags[ftype]["generic"] + href = fmt.Sprintf(hrefTemplate, fname, stype, fname) + logger.Debug(fmt.Sprintf("failed to generate thumbnail for %s, showing original image", fpath)) + } else { + hrefTemplate = htmlTags[ftype]["thumbnail"] + href = fmt.Sprintf(hrefTemplate, fname, thumbPath) + strippedDebugOutput = fmt.Sprintf(hrefTemplate, fname, logStripMsg) + } + } + } else if ftype == "application" { + if stype == "pdf" { + hrefTemplate = htmlTags[ftype][stype] + href = fmt.Sprintf(hrefTemplate, fname, fname) + } else { + unknown = true + hrefTemplate = htmlTags["unknown"]["generic"] + href = fmt.Sprintf(hrefTemplate, stype, fname, fname) + } + } else if ftype == "audio" { + hrefTemplate = htmlTags[ftype]["generic"] + href = fmt.Sprintf(hrefTemplate, fname, stype) + } else if ftype == "video" { + hrefTemplate = htmlTags[ftype]["generic"] + href = fmt.Sprintf(hrefTemplate, fname, stype) + } else { + unknown = true + hrefTemplate = htmlTags["unknown"]["generic"] + href = fmt.Sprintf(hrefTemplate, stype, fname, fname) + } + + if strippedDebugOutput != "" { + logger.Debug(fmt.Sprintf("%s was wrapped in: %s", fname, strippedDebugOutput)) + } else { + logger.Debug(fmt.Sprintf("%s was wrapped in: %s", fname, href)) + } + + return unknown, href, nil +} + +// genThumb generates a thumbnail alongside the image. +func genThumb(fpath string) (string, error) { + ext := filepath.Ext(fpath) + destPath := strings.Replace(fpath, ext, fmt.Sprintf("%s%s", thumbnailSuffix, ext), 1) + + if _, err := os.Stat(destPath); err == nil || strings.Contains(fpath, thumbnailSuffix) { + return filepath.Base(destPath), nil + } + + gen := thumb.NewGenerator(thumb.Generator{Scaler: "CatmullRom"}) + img, err := gen.NewImageFromFile(fpath) + if err != nil { + return "", fmt.Errorf("unable to read image file %s: %s", fpath, err) + } + + thumbBytes, err := gen.CreateThumbnail(img) + if err != nil { + return "", fmt.Errorf("unable to generate thumbnail for %s: %s", fpath, err) + } + + if _, err := os.Stat(destPath); errors.Is(err, os.ErrNotExist) { + if err = ioutil.WriteFile(destPath, thumbBytes, 0644); err != nil { + return "", fmt.Errorf("unable to write thumbnail to %s: %s", destPath, err) + } + logger.Debug(fmt.Sprintf("generated thumbnail for %s", fpath)) + } + + return filepath.Base(destPath), nil +} + +// shouldSkip checks if a specific file system path should be skipped over when +// running distribusi file generation. This might happen due to being a hidden +// directory or matching a pattern provided by the end-user. +func shouldSkip(fpath string, ignore []string) (bool, error) { + base := filepath.Base(fpath) + + for _, pattern := range ignore { + match, err := filepath.Match(pattern, base) + if err != nil { + return false, err + } + if match { + logger.Debug(fmt.Sprintf("skipping %s, matched %s", base, pattern)) + return true, nil + } + } + + if strings.Contains(base, thumbnailSuffix) { + logger.Debug(fmt.Sprintf("skipping %s (generated thumbnail)", base)) + return true, nil + } + + return false, nil +} + +// createIndex writes a new generated index.html file to the file system. +func createIndex(fpath string, html []string, styles string) error { body := fmt.Sprintf(htmlBody, generatedInDistribusi, "", strings.Join(html, "\n")) if styles != "" { @@ -772,7 +552,7 @@ func mkIndex(fpath string, html []string, styles string) error { return nil } - logrus.Debugf("loading custom styles from %s", absPath) + logger.Debug(fmt.Sprintf("loading custom styles from %s", absPath)) body = fmt.Sprintf(htmlBody, generatedInDistribusi, contents, strings.Join(html, "\n")) } @@ -784,23 +564,23 @@ func mkIndex(fpath string, html []string, styles string) error { if _, err := os.Stat(HTMLPath); err != nil { if os.IsNotExist(err) { if err := ioutil.WriteFile(HTMLPath, contents, 0644); err != nil { - logrus.Debugf("unable to write %s, skipping", HTMLPath) + logger.Debug(fmt.Sprintf("unable to write %s, skipping", HTMLPath)) return nil } } else { - logrus.Debugf("unable to read %s, skipping", HTMLPath) + logger.Debug(fmt.Sprintf("unable to read %s, skipping", HTMLPath)) return nil } } else { file, err := os.ReadFile(HTMLPath) if err != nil { - logrus.Debugf("unable to read %s, skipping", HTMLPath) + logger.Debug(fmt.Sprintf("unable to read %s, skipping", HTMLPath)) return nil } if strings.Contains(string(file), generatedInDistribusi) { if err := ioutil.WriteFile(HTMLPath, contents, 0644); err != nil { - logrus.Debugf("unable to write %s, skipping", HTMLPath) + logger.Debug(fmt.Sprintf("unable to write %s, skipping", HTMLPath)) return nil } } @@ -809,34 +589,159 @@ func mkIndex(fpath string, html []string, styles string) error { return nil } +// getMtype reads file mimetypes directlry from files. +func getMtype(fpath string) (string, error) { + mtype, err := mimetype.DetectFile(fpath) + if err != nil { + return "", err + } + return mtype.String(), nil +} + // serveHTTP serves a web server for browsing distribusi generated files. This // is mostly convenient when doing development work or showing something // quickly on your work station. It should be fine to serve "for production" -// though too as it uses the stdlib Go HTTP server. Distribusi still works just -// fine with the usual Nginx, Apache, etc. +// though too as it uses the stdlib Go HTTP server. Distribusi generated files +// still works just fine with the usual Nginx, Apache, etc. func serveHTTP(fpath string) error { fs := http.FileServer(http.Dir(fpath)) http.Handle("/", fs) - if err := http.ListenAndServe(port, nil); err != nil { return err } - return nil } -// wipeGeneratedFiles removes all distribusi generated files under a file -// system path. We do take care to avoid deleting files that distribusi has not -// generated by checking their contents. -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 - } - logrus.Debugf("wiping %s as requested", fpath) +// distribusify runs the main distribusi generation logic. +func distribusify(root string, ignore []string) error { + loading := "distribusifying..." + if serveFlag { + loading = fmt.Sprintf("distribusifying... live @ http://localhost%s", port) + } + fmt.Printf(loading) + + if err := filepath.Walk(root, func(fpath string, finfo os.FileInfo, err error) error { + skip, err := shouldSkip(fpath, ignore) + if err != nil { + return err } + if skip { + return nil + } + + var html []string + + if finfo.IsDir() { + var dirs []string + var files []string + + absPath, err := filepath.Abs(fpath) + if err != nil { + logger.Debug(fmt.Sprintf("unable to read %s", absPath)) + return nil + } + + contents, err := ioutil.ReadDir(absPath) + if err != nil { + logger.Debug(fmt.Sprintf("unable to read %s", absPath)) + return nil + } + + for _, content := range contents { + if content.IsDir() { + dirs = append(dirs, path.Join(absPath, content.Name())) + } else { + if content.Name() == "index.html" { + indexPath := path.Join(absPath, content.Name()) + file, err := os.ReadFile(indexPath) + if err != nil { + logger.Debug(fmt.Sprintf("unable to read %s, skipping", content.Name())) + continue + } + + if strings.Contains(string(file), generatedInDistribusi) { + logger.Debug(fmt.Sprintf("%s was not generated by distribusi, skipping", indexPath)) + continue + } + } + + files = append(files, path.Join(absPath, content.Name())) + } + } + + if len(dirs) == 0 && len(files) == 0 { + return nil + } + + if root != fpath { + href := "../" + div, err := createDiv("os/directory", href, "menu", false) + if err != nil { + return err + } + html = append(html, div) + } + + for _, dir := range dirs { + fname := filepath.Base(dir) + + skip, err := shouldSkip(fname, ignore) + if err != nil { + return err + } + if skip { + continue + } + + mtype := "os/directory" + + href := fmt.Sprintf("%s/", fname, fname) + div, err := createDiv(mtype, href, fname, false) + if err != nil { + return err + } + + html = append(html, div) + } + + for _, file := range files { + fname := filepath.Base(file) + + skip, err := shouldSkip(fname, ignore) + if err != nil { + return err + } + if skip { + continue + } + + mtype, err := getMtype(file) + if err != nil { + logger.Debug(fmt.Sprintf("failed to read mimetype of %s", file)) + continue + } + + unknown, href, err := createHref(file, mtype) + if err != nil { + logger.Debug(fmt.Sprintf("failed to generate href for %s", file)) + continue + } + + div, err := createDiv(mtype, href, fname, unknown) + if err != nil { + logger.Debug(fmt.Sprintf("failed to generate div for %s", file)) + continue + } + + html = append(html, div) + } + + if err := createIndex(absPath, html, cssFlag); err != nil { + logger.Debug(fmt.Sprintf("unable to generated %s, skipping", path.Join(absPath, "index.html"))) + return nil + } + } + return nil }); err != nil { return err @@ -844,141 +749,3 @@ func wipeGeneratedFiles(dir string) error { return nil } - -// shouldSkip checks if a specific file system path should be skipped over when -// running distribusi file generation. This might happen due to being a hidden -// directory or matching a pattern provided by the end-user. -func shouldSkip(c *cli.Context, fpath string, ignore []string) (bool, error) { - for _, pattern := range ignore { - match, err := filepath.Match(pattern, filepath.Base(fpath)) - if err != nil { - return false, err - } - if match { - logrus.Debugf("skipping %s, matched %s", filepath.Base(fpath), pattern) - return true, nil - } - } - - fpaths := strings.Split(fpath, "/") - for _, part := range fpaths { - if strings.HasPrefix(part, ".") { - if !c.Bool("hidden") { - logrus.Debugf("skipping %s, hidden directory", fpath) - return true, nil - } - } - } - - return false, nil -} - -// getLogFile creates a new log file for debug output. We do this because the -// standard debug listing is quite verbose and it is often more convenient to -// read it from file. Also handier for bug reports. -func getLogFile() (*os.File, error) { - cTime := time.Now() - timeNow := fmt.Sprintf("%v-%v-%v", cTime.Hour(), cTime.Minute(), cTime.Second()) - prefix := fmt.Sprintf("distribusi-go-%s", timeNow) - - file, err := ioutil.TempFile("/tmp", prefix) - if err != nil { - return nil, err - } - - logrus.Debugf("creating %s as debug log file", file.Name()) - - return file, nil -} - -// mkProgressBar creates a customised progress bar. This bar is used to give -// real-time updates on the progress of running distribusi. -func mkProgressBar(c *cli.Context) *progressbar.ProgressBar { - var description string - if c.Bool("serve") { - description = fmt.Sprintf("distribusifying... live @ http://localhost%s", port) - } else { - description = "distribusifying..." - } - - bar := progressbar.NewOptions(-1, - progressbar.OptionSetDescription(description), - progressbar.OptionSetWriter(ansi.NewAnsiStdout()), - progressbar.OptionEnableColorCodes(true), - progressbar.OptionShowCount(), - progressbar.OptionSpinnerType(9), - ) - - return bar -} - -// scpPublish initates a scp-like publishing interface. We do our best here to -// simply work with existing local work station SSH client configurations. It -// is mostly up to folks to configure their own shit. We don't do anything -// fancy here. -func scpPublish(c *cli.Context, root, pubTarget string) error { - split := strings.Split(pubTarget, ":") - server, remotePath := split[0], split[1] - - logrus.Debugf("parsed server: %s, remotePath: %s from %s", server, remotePath, pubTarget) - - sshUser := ssh_config.Get(server, "User") - if sshUser == "" { - logrus.Debugf("no ssh user discovered for %s, using system user as default", server) - - sysUser, err := user.Current() - if err != nil { - return fmt.Errorf("unable to determine current system user") - } - - sshUser = sysUser.Username - } - - sshPort := ssh_config.Get(server, "Port") - - sshConf := &ssh.ClientConfig{ - User: sshUser, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // awful, i know - Timeout: 5 * time.Second, - } - - identityFile := ssh_config.Get(server, "IdentityFile") - if identityFile != "" && identityFile != "~/.ssh/identity" { - sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) - if err != nil { - return fmt.Errorf("unable to connect to local ssh-agent, is it running?") - } - - agentCl := agent.NewClient(sshAgent) - authMethod := ssh.PublicKeysCallback(agentCl.Signers) - sshConf.Auth = []ssh.AuthMethod{authMethod} - - logrus.Debugf("choosing ssh key: %s to connect to %s using ssh-agent", identityFile, server) - } else { - logrus.Debugf("no ssh key discovered for %s", server) - } - - logrus.Debugf("connecting to %s with user: %s, port: %s", server, sshUser, sshPort) - - serverAndPort := fmt.Sprintf("%s:%s", server, sshPort) - scpClient, err := scp.NewClient(serverAndPort, sshConf, &scp.ClientOption{}) - if err != nil { - return fmt.Errorf("unable to make SSH connection to %s, have you configured your SSH client?", server) - } - defer scpClient.Close() - - opts := &scp.DirTransferOption{ - Context: c.Context, - Timeout: 10 * time.Minute, - } - - fmt.Printf(fmt.Sprintf("publishing %s to %s...", filepath.Base(root), server)) - - if err := scpClient.CopyDirToRemote(root, remotePath, opts); err != nil { - return fmt.Errorf("woops, publishing failed, saw this error: %s", err.Error()) - } - - fmt.Println(" done!") - - return nil -} diff --git a/distribusi_test.go b/distribusi_test.go index b44ca97..adf63af 100644 --- a/distribusi_test.go +++ b/distribusi_test.go @@ -1,19 +1,9 @@ package main import ( - "os" - "path/filepath" - "strings" "testing" ) -func TestTrimFinalNewline(t *testing.T) { - trimmed := trimFinalNewline([]byte("foo\n")) - if trimmed != "foo" { - t.Fatalf("failed to trimmed new line from 'foo\\n'") - } -} - func TestTrimAllNewlines(t *testing.T) { trimmed := trimAllNewlines("\nfoo\n") if trimmed != "foo" { @@ -43,23 +33,3 @@ func TestParseMtype(t *testing.T) { t.Fatalf("failed to error out correctly parsing %s", mtype) } } - -func TestGetLogFile(t *testing.T) { - f, err := getLogFile() - if err != nil { - t.Fatalf("failed to create log file, saw: %s", err) - } - - if !strings.Contains(f.Name(), "distribusi-go") { - t.Fatalf("log file named incorrectly: %s", f.Name()) - } - - absPath, err := filepath.Abs(f.Name()) - if err != nil { - t.Fatalf("failed to read absoluate path of %s", f.Name()) - } - - if err := os.Remove(absPath); err != nil { - t.Fatalf("unable to remove %s", absPath) - } -} diff --git a/go.mod b/go.mod index 8c80dbf..b4b966f 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,13 @@ module varia.zone/distribusi -go 1.18 +go 1.21 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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 - github.com/kevinburke/ssh_config v1.1.0 - github.com/povsister/scp v0.0.0-20210427074412-33febfd9f13e - github.com/schollz/progressbar/v3 v3.8.6 - github.com/sirupsen/logrus v1.8.1 - github.com/urfave/cli/v2 v2.3.0 - golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 + github.com/prplecake/go-thumbnail v0.1.6 ) 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/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/rivo/uniseg v0.2.0 // 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/image v0.0.0-20220302094943-723b81ca9867 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/go.sum b/go.sum index cd786b2..4e94408 100644 --- a/go.sum +++ b/go.sum @@ -1,71 +1,14 @@ -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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= -github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -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/povsister/scp v0.0.0-20210427074412-33febfd9f13e h1:VtsDti2SgX7M7jy0QAyGgb162PeHLrOaNxmcYOtaGsY= -github.com/povsister/scp v0.0.0-20210427074412-33febfd9f13e/go.mod h1:i1Au86ZXK0ZalQNyBp2njCcyhSCR/QP/AMfILip+zNI= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -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/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= -github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= -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/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +github.com/prplecake/go-thumbnail v0.1.6 h1:6mBLXL2/79QHYRCvrciiLtT6vAJfOaMAvNhmjsyLX1c= +github.com/prplecake/go-thumbnail v0.1.6/go.mod h1:v8PaF1YrWlJjdkROeoDgDil5nFycVdH7taPOl/NS830= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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/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-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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=