2021-09-30 00:57:13 +00:00
|
|
|
package filesharing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"math"
|
2021-12-14 21:21:45 +00:00
|
|
|
"os"
|
2021-09-30 22:46:10 +00:00
|
|
|
path "path/filepath"
|
2021-12-17 00:40:28 +00:00
|
|
|
"regexp"
|
2021-09-30 00:57:13 +00:00
|
|
|
"strconv"
|
2021-12-14 21:21:45 +00:00
|
|
|
"strings"
|
2021-11-04 21:07:43 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"cwtch.im/cwtch/model"
|
|
|
|
"cwtch.im/cwtch/model/attr"
|
2021-12-19 00:15:05 +00:00
|
|
|
"cwtch.im/cwtch/model/constants"
|
2021-11-04 21:07:43 +00:00
|
|
|
"cwtch.im/cwtch/peer"
|
|
|
|
"cwtch.im/cwtch/protocol/files"
|
|
|
|
"git.openprivacy.ca/openprivacy/log"
|
2021-09-30 00:57:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Functionality groups some common UI triggered functions for contacts...
|
|
|
|
type Functionality struct {
|
|
|
|
}
|
|
|
|
|
2021-11-23 20:17:11 +00:00
|
|
|
// FunctionalityGate returns filesharing if enabled in the given experiment map
|
|
|
|
// Note: Experiment maps are currently in libcwtch-go
|
2021-09-30 00:57:13 +00:00
|
|
|
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
2021-12-19 00:15:05 +00:00
|
|
|
if experimentMap[constants.FileSharingExperiment] {
|
2021-09-30 00:57:13 +00:00
|
|
|
return new(Functionality), nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("filesharing is not enabled")
|
|
|
|
}
|
|
|
|
|
2021-12-19 00:34:07 +00:00
|
|
|
// PreviewFunctionalityGate returns filesharing if image previews are enabled
|
2021-12-14 21:21:45 +00:00
|
|
|
func PreviewFunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
2021-12-19 00:34:07 +00:00
|
|
|
if experimentMap[constants.FileSharingExperiment] && experimentMap[constants.ImagePreviewsExperiment] {
|
2021-12-14 21:21:45 +00:00
|
|
|
return new(Functionality), nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("image previews are not enabled")
|
|
|
|
}
|
|
|
|
|
2021-09-30 00:57:13 +00:00
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2021-12-19 00:34:07 +00:00
|
|
|
// FileKey is the unique reference to a file offer
|
2021-12-14 21:21:45 +00:00
|
|
|
func (om *OverlayMessage) FileKey() string {
|
|
|
|
return fmt.Sprintf("%s.%s", om.Hash, om.Nonce)
|
|
|
|
}
|
|
|
|
|
2021-12-19 00:34:07 +00:00
|
|
|
// ShouldAutoDL checks file size and file name. *DOES NOT* check user settings or contact state
|
2021-12-17 00:40:28 +00:00
|
|
|
func (om *OverlayMessage) ShouldAutoDL() bool {
|
2021-12-19 00:15:05 +00:00
|
|
|
if om.Size > constants.ImagePreviewMaxSizeInBytes {
|
|
|
|
return false
|
|
|
|
}
|
2021-12-17 00:40:28 +00:00
|
|
|
lname := strings.ToLower(om.Name)
|
2021-12-19 00:34:07 +00:00
|
|
|
for _, s := range constants.AutoDLFileExts {
|
2021-12-19 00:15:05 +00:00
|
|
|
if strings.HasSuffix(lname, s) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2021-12-17 00:40:28 +00:00
|
|
|
}
|
|
|
|
|
2021-09-30 00:57:13 +00:00
|
|
|
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
|
|
|
|
// to downloadFilePath
|
2021-11-17 22:34:13 +00:00
|
|
|
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) {
|
2021-10-07 22:40:25 +00:00
|
|
|
|
|
|
|
// Store local.filesharing.filekey.manifest as the location of the manifest
|
|
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)
|
|
|
|
|
2021-11-02 22:07:24 +00:00
|
|
|
// Store local.filesharing.filekey.path as the location of the download
|
|
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), downloadFilePath)
|
2021-10-07 22:40:25 +00:00
|
|
|
|
|
|
|
// Get the value of conversation.filesharing.filekey.manifest.size from `handle`
|
2021-11-17 22:34:13 +00:00
|
|
|
profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
|
2021-09-30 00:57:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
|
|
|
|
// at filepath
|
2021-11-11 00:41:43 +00:00
|
|
|
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, conversationID int) error {
|
2021-09-30 00:57:13 +00:00
|
|
|
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))
|
2021-11-04 21:07:43 +00:00
|
|
|
|
|
|
|
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))
|
2021-10-07 22:40:25 +00:00
|
|
|
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))
|
2021-09-30 00:57:13 +00:00
|
|
|
|
|
|
|
profile.ShareFile(key, string(serializedManifest))
|
|
|
|
|
2021-11-11 00:41:43 +00:00
|
|
|
profile.SendMessage(conversationID, string(wrapperJSON))
|
2021-09-30 00:57:13 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2021-12-14 21:21:45 +00:00
|
|
|
|
2021-12-19 00:34:07 +00:00
|
|
|
// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem
|
2021-12-14 21:21:45 +00:00
|
|
|
func GenerateDownloadPath(basePath, fileName string) (filePath, manifestPath string) {
|
2021-12-17 00:40:28 +00:00
|
|
|
// 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))
|
|
|
|
}
|
2021-12-14 21:21:45 +00:00
|
|
|
filePath = fmt.Sprintf("%s%s", basePath, fileName)
|
|
|
|
manifestPath = fmt.Sprintf("%s.manifest", filePath)
|
|
|
|
|
2021-12-17 00:40:28 +00:00
|
|
|
// 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
|
2021-12-14 21:21:45 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|