package filesharing import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "math" "os" path "path/filepath" "regexp" "strconv" "strings" "time" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/files" "git.openprivacy.ca/openprivacy/log" ) // Functionality groups some common UI triggered functions for contacts... 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"] { return new(Functionality), nil } return nil, errors.New("filesharing is not enabled") } func PreviewFunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { if experimentMap["filesharing"] == true && experimentMap["filesharing-images"] == true { 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 { Name string `json:"f"` Hash string `json:"h"` Nonce string `json:"n"` Size uint64 `json:"s"` } func (om *OverlayMessage) FileKey() string { return fmt.Sprintf("%s.%s", om.Hash, om.Nonce) } // checks file size and file name. *DOES NOT* check user settings or contact state func (om *OverlayMessage) ShouldAutoDL() bool { lname := strings.ToLower(om.Name) return om.Size <= 20971520 && (strings.HasSuffix(lname, "jpg") || strings.HasSuffix(lname, "jpeg") || strings.HasSuffix(lname, "png") || strings.HasSuffix(lname, "gif") || strings.HasSuffix(lname, "webp") || strings.HasSuffix(lname, "bmp")) } // 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) { // Store local.filesharing.filekey.manifest as the location of the manifest profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath) // Store local.filesharing.filekey.path as the location of the download profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), downloadFilePath) // Get the value of conversation.filesharing.filekey.manifest.size from `handle` profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key)) } // ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file // at filepath func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, conversationID int) error { manifest, err := files.CreateManifest(filepath) if err != nil { return err } var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { log.Errorf("Cannot read from random: %v\n", err) return err } message := OverlayMessage{ Name: path.Base(manifest.FileName), Hash: hex.EncodeToString(manifest.RootHash), Nonce: hex.EncodeToString(nonce[:]), Size: manifest.FileSizeInBytes, } data, _ := json.Marshal(message) wrapper := model.MessageWrapper{ Overlay: model.OverlayFileSharing, Data: string(data), } wrapperJSON, _ := json.Marshal(wrapper) key := fmt.Sprintf("%x.%x", manifest.RootHash, nonce) serializedManifest, _ := json.Marshal(manifest) // Store the size of the manifest (in chunks) as part of the public scope so contacts who we share the file with // can fetch the manifest as if it were a file. // manifest.FileName gets redacted in filesharing_subsystem (to remove the system-specific file hierarchy), // but we need to *store* the full path because the sender also uses it to locate the file lenDiff := len(filepath) - len(path.Base(filepath)) profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10)) profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest)) profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize))))) profile.ShareFile(key, string(serializedManifest)) profile.SendMessage(conversationID, string(wrapperJSON)) return nil } 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) } }