diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index c767352..8d76ddf 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -8,12 +8,16 @@ import ( "fmt" "io" "math" + "os" path "path/filepath" + "regexp" "strconv" + "strings" "time" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/files" "git.openprivacy.ca/openprivacy/log" @@ -26,12 +30,20 @@ type Functionality struct { // FunctionalityGate returns filesharing if enabled in the given experiment map // Note: Experiment maps are currently in libcwtch-go func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { - if experimentMap["filesharing"] { + if experimentMap[constants.FileSharingExperiment] { return new(Functionality), nil } return nil, errors.New("filesharing is not enabled") } +// PreviewFunctionalityGate returns filesharing if image previews are enabled +func PreviewFunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { + if experimentMap[constants.FileSharingExperiment] && experimentMap[constants.ImagePreviewsExperiment] { + return new(Functionality), nil + } + return nil, errors.New("image previews are not enabled") +} + // OverlayMessage presents the canonical format of the File Sharing functionality Overlay Message // This is the format that the UI will parse to display the message type OverlayMessage struct { @@ -41,6 +53,25 @@ type OverlayMessage struct { Size uint64 `json:"s"` } +// FileKey is the unique reference to a file offer +func (om *OverlayMessage) FileKey() string { + return fmt.Sprintf("%s.%s", om.Hash, om.Nonce) +} + +// ShouldAutoDL checks file size and file name. *DOES NOT* check user settings or contact state +func (om *OverlayMessage) ShouldAutoDL() bool { + if om.Size > constants.ImagePreviewMaxSizeInBytes { + return false + } + lname := strings.ToLower(om.Name) + for _, s := range constants.AutoDLFileExts { + if strings.HasSuffix(lname, s) { + return true + } + } + return false +} + // DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process // to downloadFilePath func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) { @@ -103,3 +134,44 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, conve return nil } + +// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem +func GenerateDownloadPath(basePath, fileName string) (filePath, manifestPath string) { + // avoid all kina funky shit + re := regexp.MustCompile(`[^A-Za-z0-9._-]`) + filePath = re.ReplaceAllString(filePath, "") + // avoid hidden files on linux + for strings.HasPrefix(filePath, ".") { + filePath = strings.TrimPrefix(filePath, ".") + } + // avoid empties + if strings.TrimSpace(filePath) == "" { + filePath = "untitled" + } + // if you like it, put a / on it + if !strings.HasSuffix(basePath, string(os.PathSeparator)) { + basePath = fmt.Sprintf("%s%s", basePath, string(os.PathSeparator)) + } + filePath = fmt.Sprintf("%s%s", basePath, fileName) + manifestPath = fmt.Sprintf("%s.manifest", filePath) + + // if file is named "file", iterate "file", "file (2)", "file (3)", ... until DNE + // if file is named "file.ext", iterate "file.ext", "file (2).ext", "file (3).ext", ... until DNE + parts := strings.Split(fileName, ".") + fileNameBase := parts[0] + fileNameExt := "" + if len(parts) > 1 { + fileNameBase = strings.Join(parts[0:len(parts)-1], ".") + fileNameExt = fmt.Sprintf(".%s", parts[len(parts)-1]) + } + + for i := 2; ; i++ { + if _, err := os.Open(filePath); os.IsNotExist(err) { + if _, err := os.Open(manifestPath); os.IsNotExist(err) { + return + } + } + filePath = fmt.Sprintf("%s%s (%d)%s", basePath, fileNameBase, i, fileNameExt) + manifestPath = fmt.Sprintf("%s.manifest", filePath) + } +} diff --git a/model/constants/experiments.go b/model/constants/experiments.go new file mode 100644 index 0000000..665a856 --- /dev/null +++ b/model/constants/experiments.go @@ -0,0 +1,14 @@ +package constants + +// FileSharingExperiment Allows file sharing +const FileSharingExperiment = "filesharing" + +// ImagePreviewsExperiment Causes images (up to ImagePreviewMaxSizeInBytes, from accepted contacts) to auto-dl and preview +// requires FileSharingExperiment to be enabled +const ImagePreviewsExperiment = "filesharing-images" + +// ImagePreviewMaxSizeInBytes Files up to this size will be autodownloaded using ImagePreviewsExperiment +const ImagePreviewMaxSizeInBytes = 20971520 + +// AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment +var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"} diff --git a/testing/filesharing/file_sharing_integration_test.go b/testing/filesharing/file_sharing_integration_test.go index 5c1bd13..47a604f 100644 --- a/testing/filesharing/file_sharing_integration_test.go +++ b/testing/filesharing/file_sharing_integration_test.go @@ -2,6 +2,11 @@ package filesharing import ( "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + app2 "cwtch.im/cwtch/app" "cwtch.im/cwtch/app/utils" "cwtch.im/cwtch/event" @@ -12,14 +17,10 @@ import ( "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" "cwtch.im/cwtch/protocol/files" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" + // Import SQL Cipher - _ "github.com/mutecomm/go-sqlcipher/v4" mrand "math/rand" "os" "os/user" @@ -28,6 +29,8 @@ import ( "runtime/pprof" "testing" "time" + + _ "github.com/mutecomm/go-sqlcipher/v4" ) func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) { @@ -121,7 +124,7 @@ func TestFileSharing(t *testing.T) { fmt.Println("Alice and Bob are Connected!!") - filesharingFunctionality, _ := filesharing.FunctionalityGate(map[string]bool{"filesharing": true}) + filesharingFunctionality, _ := filesharing.FunctionalityGate(map[string]bool{constants.FileSharingExperiment: true}) err = filesharingFunctionality.ShareFile("cwtch.png", alice, 1)