More improvements to fileopening

* Add safe-guard to avoid nuking a whole hard-drive due to a potential bug
* save files with their attachment UUID instead of filename, to avoid
clashes
* move main-code of the feature into a new package (file)
This commit is contained in:
Marcel Schramm 2020-08-22 15:36:49 +02:00
parent 4db341e20b
commit c0152662f0
No known key found for this signature in database
GPG Key ID: 05971054C70EEDC7
2 changed files with 96 additions and 75 deletions

82
fileopen/fileopen.go Normal file
View File

@ -0,0 +1,82 @@
package fileopen
import (
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/Bios-Marcel/cordless/commands"
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/util/files"
"github.com/skratchdot/open-golang/open"
)
var cacheCleanerLock = &sync.Mutex{}
// LaunchCacheCleaner clears all files in the given folder structure that are
// older than the given timeframe. Root-Paths are ignored for safety reasons.
func LaunchCacheCleaner(targetFolder string, olderThan time.Duration) {
//We try to avoid deleting someones whole hard-drive-content.
//Is there a better way to do this?
if targetFolder == "" || targetFolder == "/" || (len(targetFolder) == 3 && strings.HasSuffix(targetFolder, ":/")) {
return
}
go func() {
cacheCleanerLock.Lock()
defer cacheCleanerLock.Unlock()
now := time.Now().UnixNano()
filepath.Walk(targetFolder, func(path string, f os.FileInfo, err error) error {
if now-f.ModTime().UnixNano() >= olderThan.Nanoseconds() {
removeError := os.Remove(path)
if removeError != nil {
log.Printf("Couldn't remove file %s from cache.\n", removeError)
}
}
return nil
})
}()
}
// OpenFile attempts downloading and opening a file from the given link.
// Files are either cached locally or saved permanently. In both cases
// cordless attempts loading the previously downloaded version of the
// file. Files are saved using the ID of the attachment in order to avoid
// false positives when doing cache-matching.
func OpenFile(targetFolder, fileID, downloadURL string) error {
extension := strings.TrimPrefix(filepath.Ext(downloadURL), ".")
targetFile := filepath.Join(targetFolder, fileID+"."+extension)
downloadError := files.DownloadFileOrAccessCache(targetFile, downloadURL)
if downloadError != nil {
return downloadError
}
handler, handlerSet := config.Current.FileOpenHandlers[extension]
if handlerSet {
handlerTrimmed := strings.TrimSpace(handler)
//Empty means to not open files with the given extension.
if handlerTrimmed == "" {
log.Printf("skip opening link %s, as the extension %s has been disabled.\n", downloadURL, extension)
return nil
}
commandParts := commands.ParseCommand(strings.ReplaceAll(handlerTrimmed, "{$file}", targetFile))
command := exec.Command(commandParts[0], commandParts[1:]...)
startError := command.Start()
if startError != nil {
return startError
}
} else {
openError := open.Run(targetFile)
if openError != nil {
return openError
}
}
return nil
}

View File

@ -5,17 +5,15 @@ import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"unicode"
"github.com/mattn/go-runewidth"
"github.com/mdp/qrterminal/v3"
"github.com/skratchdot/open-golang/open"
"github.com/Bios-Marcel/cordless/fileopen"
"github.com/Bios-Marcel/cordless/util/files"
"github.com/Bios-Marcel/cordless/util/fuzzy"
"github.com/Bios-Marcel/cordless/util/text"
@ -98,8 +96,6 @@ type Window struct {
bareChat bool
activeView ActiveView
cacheClearLock *sync.Mutex
}
type ActiveView bool
@ -118,7 +114,6 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
activeView: Guilds,
extensionEngines: []scripting.Engine{js.New()},
messageLoader: discordutil.CreateMessageLoader(session),
cacheClearLock: &sync.Mutex{},
}
if config.Current.DesktopNotificationsUserInactivityThreshold > 0 {
@ -370,11 +365,6 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
}
if shortcuts.ViewSelectedMessageImages.Equals(event) {
links := make([]string, 0, len(message.Attachments))
for _, file := range message.Attachments {
links = append(links, file.URL)
}
var targetFolder string
if config.Current.FileOpenSaveFilesPermanently {
@ -394,43 +384,26 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
window.ShowCustomErrorDialog("Couldn't open file", "Can't create cache subdirectory.")
return nil
}
//If permanent saving isn't disabled, we clear files older
//than one month whenever something is opened. Since this
//will happen in a background thread, it won't cause
//application blocking.
if !config.Current.FileOpenSaveFilesPermanently {
defer func() {
go func() {
window.cacheClearLock.Lock()
now := time.Now().Hour()
twoWeeks := 24 * 14
filepath.Walk(targetFolder, func(path string, f os.FileInfo, err error) error {
if now-f.ModTime().Hour() >= twoWeeks {
removeError := os.Remove(path)
if removeError != nil {
log.Printf("Couldn't remove file %s from cache.\n", removeError)
}
}
return nil
})
defer window.cacheClearLock.Unlock()
}()
}()
}
}
}
if targetFolder == "" {
window.ShowCustomErrorDialog("Couldn't open file", "Can't find cache directory.")
} else {
for _, file := range message.Attachments {
openError := fileopen.OpenFile(targetFolder, file.ID, file.URL)
if openError != nil {
window.ShowCustomErrorDialog("Couldn't open file", openError.Error())
}
}
}
for _, link := range links {
openError := window.openFile(targetFolder, link)
if openError != nil {
window.ShowCustomErrorDialog("Couldn't open file", openError.Error())
}
//If permanent saving isn't disabled, we clear files older
//than one month whenever something is opened. Since this
//will happen in a background thread, it won't cause
//application blocking.
if !config.Current.FileOpenSaveFilesPermanently && targetFolder != "" {
fileopen.LaunchCacheCleaner(targetFolder, time.Hour*(24*14))
}
return nil
@ -1284,40 +1257,6 @@ important changes of the last two versions officially released.
`, version.Version)
}
func (window *Window) openFile(targetFolder, link string) error {
targetFile := filepath.Join(targetFolder, filepath.Base(link))
downloadError := files.DownloadFileOrAccessCache(targetFile, link)
if downloadError != nil {
return downloadError
}
extension := strings.TrimPrefix(filepath.Ext(targetFile), ".")
handler, handlerSet := config.Current.FileOpenHandlers[extension]
if handlerSet {
handlerTrimmed := strings.TrimSpace(handler)
//Empty means to not open files with the given extension.
if handlerTrimmed == "" {
log.Printf("skip opening link %s, as the extension %s has been disabled.\n", link, extension)
return nil
}
commandParts := commands.ParseCommand(strings.ReplaceAll(handlerTrimmed, "{$file}", targetFile))
command := exec.Command(commandParts[0], commandParts[1:]...)
startError := command.Start()
if startError != nil {
return startError
}
} else {
log.Println("Attempting to open file: " + targetFile)
openError := open.Run(targetFile)
if openError != nil {
return openError
}
}
return nil
}
// initExtensionEngine injections necessary functions into the engine.
// those functions can be called by each script inside of an engine.
func (window *Window) initExtensionEngine(engine scripting.Engine) error {