Mediatypes support (#134)
Co-authored-by: makeworld <25111343+makeworld-the-better-one@users.noreply.github.com> Co-authored-by: Stephen Robinson <stephen@drsudo.com>
This commit is contained in:
parent
39290b09c6
commit
0df5effdcf
|
@ -116,6 +116,8 @@ Features in *italics* are in the master branch, but not in the latest release.
|
||||||
- Disabled by default, enable in config
|
- Disabled by default, enable in config
|
||||||
- [x] Proxying
|
- [x] Proxying
|
||||||
- Schemes like Gopher or HTTP can be proxied through a Gemini server
|
- Schemes like Gopher or HTTP can be proxied through a Gemini server
|
||||||
|
- [x] *Configure applications to open particular mediatypes*
|
||||||
|
- [ ] Allow piping/streaming content instead of downloading it first
|
||||||
- [x] Client certificate support
|
- [x] Client certificate support
|
||||||
- [ ] Full client certificate UX within the client
|
- [ ] Full client certificate UX within the client
|
||||||
- Create transient and permanent certs within the client, per domain
|
- Create transient and permanent certs within the client, per domain
|
||||||
|
|
|
@ -38,6 +38,7 @@ var bkmkDir string
|
||||||
var bkmkPath string
|
var bkmkPath string
|
||||||
|
|
||||||
var DownloadsDir string
|
var DownloadsDir string
|
||||||
|
var TempDownloadsDir string
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
var subscriptionDir string
|
var subscriptionDir string
|
||||||
|
@ -46,6 +47,13 @@ var SubscriptionPath string
|
||||||
// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config.
|
// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config.
|
||||||
var HTTPCommand []string
|
var HTTPCommand []string
|
||||||
|
|
||||||
|
type MediaHandler struct {
|
||||||
|
Cmd []string
|
||||||
|
NoPrompt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var MediaHandlers = make(map[string]MediaHandler)
|
||||||
|
|
||||||
func Init() error {
|
func Init() error {
|
||||||
|
|
||||||
// *** Set paths ***
|
// *** Set paths ***
|
||||||
|
@ -194,6 +202,36 @@ func Init() error {
|
||||||
DownloadsDir = dDir
|
DownloadsDir = dDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup temporary downloads dir
|
||||||
|
if viper.GetString("a-general.temp_downloads") == "" {
|
||||||
|
TempDownloadsDir = filepath.Join(os.TempDir(), "amfora_temp")
|
||||||
|
|
||||||
|
// Make sure it exists
|
||||||
|
err = os.MkdirAll(TempDownloadsDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("temp downloads path could not be created: %s", TempDownloadsDir)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate path
|
||||||
|
dDir := viper.GetString("a-general.temp_downloads")
|
||||||
|
di, err := os.Stat(dDir)
|
||||||
|
if err == nil {
|
||||||
|
if !di.IsDir() {
|
||||||
|
return fmt.Errorf("temp downloads path specified is not a directory: %s", dDir)
|
||||||
|
}
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
// Try to create path
|
||||||
|
err = os.MkdirAll(dDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("temp downloads path could not be created: %s", dDir)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Some other error
|
||||||
|
return fmt.Errorf("couldn't access temp downloads directory: %s", dDir)
|
||||||
|
}
|
||||||
|
TempDownloadsDir = dDir
|
||||||
|
}
|
||||||
|
|
||||||
// *** Setup vipers ***
|
// *** Setup vipers ***
|
||||||
|
|
||||||
TofuStore.SetConfigFile(tofuDBPath)
|
TofuStore.SetConfigFile(tofuDBPath)
|
||||||
|
@ -228,6 +266,7 @@ func Init() error {
|
||||||
viper.SetDefault("a-general.left_margin", 0.15)
|
viper.SetDefault("a-general.left_margin", 0.15)
|
||||||
viper.SetDefault("a-general.max_width", 100)
|
viper.SetDefault("a-general.max_width", 100)
|
||||||
viper.SetDefault("a-general.downloads", "")
|
viper.SetDefault("a-general.downloads", "")
|
||||||
|
viper.SetDefault("a-general.temp_downloads", "")
|
||||||
viper.SetDefault("a-general.page_max_size", 2097152)
|
viper.SetDefault("a-general.page_max_size", 2097152)
|
||||||
viper.SetDefault("a-general.page_max_time", 10)
|
viper.SetDefault("a-general.page_max_time", 10)
|
||||||
viper.SetDefault("a-general.emoji_favicons", false)
|
viper.SetDefault("a-general.emoji_favicons", false)
|
||||||
|
@ -279,5 +318,26 @@ func Init() error {
|
||||||
HTTPCommand = strings.Fields(viper.GetString("a-general.http"))
|
HTTPCommand = strings.Fields(viper.GetString("a-general.http"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rawMediaHandlers []struct {
|
||||||
|
Cmd []string `mapstructure:"cmd"`
|
||||||
|
Types []string `mapstructure:"types"`
|
||||||
|
NoPrompt bool `mapstructure:"no_prompt"`
|
||||||
|
}
|
||||||
|
err = viper.UnmarshalKey("mediatype-handlers", &rawMediaHandlers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't parse mediatype-handlers section in config: %w", err)
|
||||||
|
}
|
||||||
|
for _, rawMediaHandler := range rawMediaHandlers {
|
||||||
|
for _, typ := range rawMediaHandler.Types {
|
||||||
|
if _, ok := MediaHandlers[typ]; ok {
|
||||||
|
return fmt.Errorf("multiple mediatype-handlers defined for %v", typ)
|
||||||
|
}
|
||||||
|
MediaHandlers[typ] = MediaHandler{
|
||||||
|
Cmd: rawMediaHandler.Cmd,
|
||||||
|
NoPrompt: rawMediaHandler.NoPrompt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,54 @@ shift_numbers = "!@#$%^&*()"
|
||||||
other = 'off'
|
other = 'off'
|
||||||
|
|
||||||
|
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# Specify what applications will open certain media types.
|
||||||
|
# By default your default application will be used to open the file when you select "Open".
|
||||||
|
# You only need to configure this section if you want to override your default application,
|
||||||
|
# or do special things like streaming.
|
||||||
|
#
|
||||||
|
# To open jpeg files with the feh command:
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["feh"]
|
||||||
|
# types = ["image/jpeg"]
|
||||||
|
#
|
||||||
|
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
|
||||||
|
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
|
||||||
|
#
|
||||||
|
# If the subtype is omitted then the specified command will be used for the
|
||||||
|
# entire type:
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# command = ["vlc", "--flag"]
|
||||||
|
# types = ["audio", "video"]
|
||||||
|
#
|
||||||
|
# A catch-all handler can by specified with "*".
|
||||||
|
# Note that there are already catch-all handlers in place for all OSes,
|
||||||
|
# that open the file using your default application. This is only if you
|
||||||
|
# want to override that.
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["some-command"]
|
||||||
|
# types = [
|
||||||
|
# "application/pdf",
|
||||||
|
# "*",
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# If you want to always open a type in its viewer without the download or open
|
||||||
|
# prompt appearing, you can add no_prompt = true
|
||||||
|
#
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["feh"]
|
||||||
|
# types = ["image"]
|
||||||
|
# no_prompt = true
|
||||||
|
#
|
||||||
|
# Note: Multiple handlers cannot be defined for the same full media type, but
|
||||||
|
# still there needs to be an order for which handlers are used. The following
|
||||||
|
# order applies regardless of the order written in the config:
|
||||||
|
#
|
||||||
|
# 1. Full media type: "image/jpeg"
|
||||||
|
# 2. Just type: "image"
|
||||||
|
# 3. Catch-all: "*"
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
# Options for page cache - which is only for text/gemini pages
|
# Options for page cache - which is only for text/gemini pages
|
||||||
# Increase the cache size to speed up browsing at the expense of memory
|
# Increase the cache size to speed up browsing at the expense of memory
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
head -n 3 default.go | tee default.go > /dev/null
|
cat > default.go <<-EOF
|
||||||
|
package config
|
||||||
|
|
||||||
|
//go:generate ./default.sh
|
||||||
|
EOF
|
||||||
echo -n 'var defaultConf = []byte(`' >> default.go
|
echo -n 'var defaultConf = []byte(`' >> default.go
|
||||||
cat ../default-config.toml >> default.go
|
cat ../default-config.toml >> default.go
|
||||||
echo '`)' >> default.go
|
echo '`)' >> default.go
|
|
@ -112,6 +112,54 @@ shift_numbers = "!@#$%^&*()"
|
||||||
other = 'off'
|
other = 'off'
|
||||||
|
|
||||||
|
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# Specify what applications will open certain media types.
|
||||||
|
# By default your default application will be used to open the file when you select "Open".
|
||||||
|
# You only need to configure this section if you want to override your default application,
|
||||||
|
# or do special things like streaming.
|
||||||
|
#
|
||||||
|
# To open jpeg files with the feh command:
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["feh"]
|
||||||
|
# types = ["image/jpeg"]
|
||||||
|
#
|
||||||
|
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
|
||||||
|
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
|
||||||
|
#
|
||||||
|
# If the subtype is omitted then the specified command will be used for the
|
||||||
|
# entire type:
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# command = ["vlc", "--flag"]
|
||||||
|
# types = ["audio", "video"]
|
||||||
|
#
|
||||||
|
# A catch-all handler can by specified with "*".
|
||||||
|
# Note that there are already catch-all handlers in place for all OSes,
|
||||||
|
# that open the file using your default application. This is only if you
|
||||||
|
# want to override that.
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["some-command"]
|
||||||
|
# types = [
|
||||||
|
# "application/pdf",
|
||||||
|
# "*",
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# If you want to always open a type in its viewer without the download or open
|
||||||
|
# prompt appearing, you can add no_prompt = true
|
||||||
|
#
|
||||||
|
# [[mediatype-handlers]]
|
||||||
|
# cmd = ["feh"]
|
||||||
|
# types = ["image"]
|
||||||
|
# no_prompt = true
|
||||||
|
#
|
||||||
|
# Note: Multiple handlers cannot be defined for the same full media type, but
|
||||||
|
# still there needs to be an order for which handlers are used. The following
|
||||||
|
# order applies regardless of the order written in the config:
|
||||||
|
#
|
||||||
|
# 1. Full media type: "image/jpeg"
|
||||||
|
# 2. Just type: "image"
|
||||||
|
# 3. Catch-all: "*"
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
# Options for page cache - which is only for text/gemini pages
|
# Options for page cache - which is only for text/gemini pages
|
||||||
# Increase the cache size to speed up browsing at the expense of memory
|
# Increase the cache size to speed up browsing at the expense of memory
|
||||||
|
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -15,15 +17,16 @@ import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/makeworld-the-better-one/amfora/config"
|
"github.com/makeworld-the-better-one/amfora/config"
|
||||||
"github.com/makeworld-the-better-one/amfora/structs"
|
"github.com/makeworld-the-better-one/amfora/structs"
|
||||||
|
"github.com/makeworld-the-better-one/amfora/sysopen"
|
||||||
"github.com/makeworld-the-better-one/go-gemini"
|
"github.com/makeworld-the-better-one/go-gemini"
|
||||||
"github.com/makeworld-the-better-one/progressbar/v3"
|
"github.com/makeworld-the-better-one/progressbar/v3"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gitlab.com/tslocum/cview"
|
"gitlab.com/tslocum/cview"
|
||||||
)
|
)
|
||||||
|
|
||||||
// For choosing between download and the portal - copy of YesNo basically
|
// For choosing between download and opening - copy of YesNo basically
|
||||||
var dlChoiceModal = cview.NewModal().
|
var dlChoiceModal = cview.NewModal().
|
||||||
AddButtons([]string{"Download", "Open in portal", "Cancel"})
|
AddButtons([]string{"Open", "Download", "Cancel"})
|
||||||
|
|
||||||
// Channel to indicate what choice they made using the button text
|
// Channel to indicate what choice they made using the button text
|
||||||
var dlChoiceCh = make(chan string)
|
var dlChoiceCh = make(chan string)
|
||||||
|
@ -83,56 +86,106 @@ func dlInit() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMediaHandler(resp *gemini.Response) config.MediaHandler {
|
||||||
|
def := config.MediaHandler{
|
||||||
|
Cmd: nil,
|
||||||
|
NoPrompt: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
mediatype, _, err := mime.ParseMediaType(resp.Meta)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret, ok := config.MediaHandlers[mediatype]; ok {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
splitType := strings.Split(mediatype, "/")[0]
|
||||||
|
if ret, ok := config.MediaHandlers[splitType]; ok {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret, ok := config.MediaHandlers["*"]; ok {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
// dlChoice displays the download choice modal and acts on the user's choice.
|
// dlChoice displays the download choice modal and acts on the user's choice.
|
||||||
// It should run in a goroutine.
|
// It should run in a goroutine.
|
||||||
func dlChoice(text, u string, resp *gemini.Response) {
|
func dlChoice(text, u string, resp *gemini.Response) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
parsed, err := url.Parse(u)
|
mediaHandler := getMediaHandler(resp)
|
||||||
if err != nil {
|
var choice string
|
||||||
Error("URL Error", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if mediaHandler.NoPrompt {
|
||||||
|
choice = "Open"
|
||||||
|
} else {
|
||||||
dlChoiceModal.SetText(text)
|
dlChoiceModal.SetText(text)
|
||||||
tabPages.ShowPage("dlChoice")
|
tabPages.ShowPage("dlChoice")
|
||||||
tabPages.SendToFront("dlChoice")
|
tabPages.SendToFront("dlChoice")
|
||||||
App.SetFocus(dlChoiceModal)
|
App.SetFocus(dlChoiceModal)
|
||||||
App.Draw()
|
App.Draw()
|
||||||
|
choice = <-dlChoiceCh
|
||||||
|
}
|
||||||
|
|
||||||
choice := <-dlChoiceCh
|
|
||||||
if choice == "Download" {
|
if choice == "Download" {
|
||||||
tabPages.HidePage("dlChoice")
|
tabPages.HidePage("dlChoice")
|
||||||
App.Draw()
|
App.Draw()
|
||||||
downloadURL(u, resp)
|
downloadURL(config.DownloadsDir, u, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if choice == "Open in portal" {
|
if choice == "Open" {
|
||||||
// Open in mozz's proxy
|
tabPages.HidePage("dlChoice")
|
||||||
portalURL := u
|
App.Draw()
|
||||||
if parsed.RawQuery != "" {
|
open(u, resp)
|
||||||
// Remove query and add encoded version on the end
|
return
|
||||||
query := parsed.RawQuery
|
|
||||||
parsed.RawQuery = ""
|
|
||||||
portalURL = parsed.String() + "%3F" + query
|
|
||||||
}
|
}
|
||||||
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
|
|
||||||
ok := handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
|
|
||||||
if ok {
|
|
||||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||||
App.SetFocus(tabs[curTab].view)
|
App.SetFocus(tabs[curTab].view)
|
||||||
App.Draw()
|
App.Draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// open performs the same actions as downloadURL except it also opens the file.
|
||||||
|
// If there is no system viewer configured for the particular mediatype, it opens it
|
||||||
|
// with the default system viewer.
|
||||||
|
func open(u string, resp *gemini.Response) {
|
||||||
|
mediaHandler := getMediaHandler(resp)
|
||||||
|
path := downloadURL(config.TempDownloadsDir, u, resp)
|
||||||
|
if path == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||||
App.SetFocus(tabs[curTab].view)
|
App.SetFocus(tabs[curTab].view)
|
||||||
App.Draw()
|
App.Draw()
|
||||||
|
if mediaHandler.Cmd == nil {
|
||||||
|
// Open with system default viewer
|
||||||
|
_, err := sysopen.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
Error("System Viewer Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Info("Opened in default system viewer")
|
||||||
|
} else {
|
||||||
|
cmd := mediaHandler.Cmd
|
||||||
|
err := exec.Command(cmd[0], append(cmd[1:], path)...).Start()
|
||||||
|
if err != nil {
|
||||||
|
Error("File Opening Error", "Error executing custom command: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Info("Opened with " + cmd[0])
|
||||||
|
}
|
||||||
|
App.SetFocus(dlModal)
|
||||||
|
App.Draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadURL pulls up a modal to show download progress and saves the URL content.
|
// downloadURL pulls up a modal to show download progress and saves the URL content.
|
||||||
// downloadPage should be used for Page content.
|
// downloadPage should be used for Page content.
|
||||||
func downloadURL(u string, resp *gemini.Response) {
|
// Returns location downloaded to or an empty string on error.
|
||||||
|
func downloadURL(dir, u string, resp *gemini.Response) string {
|
||||||
_, _, width, _ := dlModal.GetInnerRect()
|
_, _, width, _ := dlModal.GetInnerRect()
|
||||||
// Copy of progressbar.DefaultBytesSilent with custom width
|
// Copy of progressbar.DefaultBytesSilent with custom width
|
||||||
bar := progressbar.NewOptions64(
|
bar := progressbar.NewOptions64(
|
||||||
|
@ -146,15 +199,15 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||||
)
|
)
|
||||||
bar.RenderBlank() //nolint:errcheck
|
bar.RenderBlank() //nolint:errcheck
|
||||||
|
|
||||||
savePath, err := downloadNameFromURL(u, "")
|
savePath, err := downloadNameFromURL(dir, u, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Download Error", "Error deciding on file name: "+err.Error())
|
Error("Download Error", "Error deciding on file name: "+err.Error())
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("Download Error", "Error creating download file: "+err.Error())
|
Error("Download Error", "Error creating download file: "+err.Error())
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
@ -184,7 +237,7 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||||
Error("Download Error", err.Error())
|
Error("Download Error", err.Error())
|
||||||
f.Close()
|
f.Close()
|
||||||
os.Remove(savePath) // Remove partial file
|
os.Remove(savePath) // Remove partial file
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
|
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
|
||||||
dlModal.ClearButtons()
|
dlModal.ClearButtons()
|
||||||
|
@ -192,6 +245,8 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||||
dlModal.GetForm().SetFocus(100)
|
dlModal.GetForm().SetFocus(100)
|
||||||
App.SetFocus(dlModal)
|
App.SetFocus(dlModal)
|
||||||
App.Draw()
|
App.Draw()
|
||||||
|
|
||||||
|
return savePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadPage saves the passed Page to a file.
|
// downloadPage saves the passed Page to a file.
|
||||||
|
@ -202,9 +257,9 @@ func downloadPage(p *structs.Page) (string, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if p.Mediatype == structs.TextGemini {
|
if p.Mediatype == structs.TextGemini {
|
||||||
savePath, err = downloadNameFromURL(p.URL, ".gmi")
|
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".gmi")
|
||||||
} else {
|
} else {
|
||||||
savePath, err = downloadNameFromURL(p.URL, ".txt")
|
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".txt")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -221,13 +276,13 @@ func downloadPage(p *structs.Page) (string, error) {
|
||||||
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
|
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
|
||||||
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
|
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
|
||||||
// It should include the dot.
|
// It should include the dot.
|
||||||
func downloadNameFromURL(u string, ext string) (string, error) {
|
func downloadNameFromURL(dir, u, ext string) (string, error) {
|
||||||
var name string
|
var name string
|
||||||
var err error
|
var err error
|
||||||
parsed, _ := url.Parse(u)
|
parsed, _ := url.Parse(u)
|
||||||
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
|
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
|
||||||
// No file, just the root domain
|
// No file, just the root domain
|
||||||
name, err = getSafeDownloadName(parsed.Hostname()+ext, true, 0)
|
name, err = getSafeDownloadName(dir, parsed.Hostname()+ext, true, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -238,23 +293,23 @@ func downloadNameFromURL(u string, ext string) (string, error) {
|
||||||
// No extension
|
// No extension
|
||||||
name += ext
|
name += ext
|
||||||
}
|
}
|
||||||
name, err = getSafeDownloadName(name, false, 0)
|
name, err = getSafeDownloadName(dir, name, false, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filepath.Join(config.DownloadsDir, name), nil
|
return filepath.Join(dir, name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSafeDownloadName is used by downloads.go only.
|
// getSafeDownloadName is used by downloads.go only.
|
||||||
// It returns a modified name that is unique for the downloads folder.
|
// It returns a modified name that is unique for the specified folder.
|
||||||
// This way duplicate saved files will not overwrite each other.
|
// This way duplicate saved files will not overwrite each other.
|
||||||
//
|
//
|
||||||
// lastDot should be set to true if the number added to the name should come before
|
// lastDot should be set to true if the number added to the name should come before
|
||||||
// the last dot in the filename instead of the first.
|
// the last dot in the filename instead of the first.
|
||||||
//
|
//
|
||||||
// n should be set to 0, it is used for recursiveness.
|
// n should be set to 0, it is used for recursiveness.
|
||||||
func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
func getSafeDownloadName(dir, name string, lastDot bool, n int) (string, error) {
|
||||||
// newName("test.txt", 3) -> "test(3).txt"
|
// newName("test.txt", 3) -> "test(3).txt"
|
||||||
newName := func() string {
|
newName := func() string {
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
|
@ -271,7 +326,7 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||||
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
|
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := os.Open(config.DownloadsDir)
|
d, err := os.Open(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -285,7 +340,7 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||||
for i := range files {
|
for i := range files {
|
||||||
if nn == files[i] {
|
if nn == files[i] {
|
||||||
d.Close()
|
d.Close()
|
||||||
return getSafeDownloadName(name, lastDot, n+1)
|
return getSafeDownloadName(dir, name, lastDot, n+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.Close()
|
d.Close()
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package sysopen
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
// Open opens `path` in default system viewer.
|
||||||
|
func Open(path string) (string, error) {
|
||||||
|
err := exec.Command("open", path).Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "Opened in default system viewer", nil
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
|
||||||
|
|
||||||
|
package sysopen
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Open opens `path` in default system viewer, but not on this OS.
|
||||||
|
func Open(path string) (string, error) {
|
||||||
|
return "", fmt.Errorf("unsupported OS for default system viewer. " +
|
||||||
|
"Set a catch-all [[mediatype-handlers]] command in the config")
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// +build linux freebsd netbsd openbsd
|
||||||
|
|
||||||
|
//nolint:goerr113
|
||||||
|
package sysopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open opens `path` in default system viewer. It tries to do so using
|
||||||
|
// xdg-open. It only works if there is a display server working.
|
||||||
|
func Open(path string) (string, error) {
|
||||||
|
var (
|
||||||
|
xorgDisplay = os.Getenv("DISPLAY")
|
||||||
|
waylandDisplay = os.Getenv("WAYLAND_DISPLAY")
|
||||||
|
xdgOpenPath, xdgOpenNotFoundErr = exec.LookPath("xdg-open")
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case xorgDisplay == "" && waylandDisplay == "":
|
||||||
|
return "", fmt.Errorf("no display server was found. " +
|
||||||
|
"You may set a default [[mediatype-handlers]] command in the config")
|
||||||
|
case xdgOpenNotFoundErr == nil:
|
||||||
|
// Use start rather than run or output in order
|
||||||
|
// to make application run in background.
|
||||||
|
if err := exec.Command(xdgOpenPath, path).Start(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "Opened in default system viewer", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("could not determine default system viewer. " +
|
||||||
|
"Set a catch-all [[mediatype-handlers]] command in the config")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// +build windows
|
||||||
|
// +build !linux !darwin !freebsd !netbsd !openbsd
|
||||||
|
|
||||||
|
package sysopen
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
// Open opens `path` in default system vierwer.
|
||||||
|
func Open(path string) (string, error) {
|
||||||
|
err := exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "Opened in default system viewer", nil
|
||||||
|
}
|
Loading…
Reference in New Issue