558 lines
23 KiB
Go
558 lines
23 KiB
Go
package filesharing
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"cwtch.im/cwtch/event"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/bits"
|
|
"os"
|
|
path "path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"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"
|
|
)
|
|
|
|
// Functionality groups some common UI triggered functions for contacts...
|
|
type Functionality struct {
|
|
}
|
|
|
|
func (f Functionality) EventsToRegister() []event.Type {
|
|
return []event.Type{event.ManifestReceived, event.FileDownloaded}
|
|
}
|
|
|
|
func (f Functionality) ExperimentsToRegister() []string {
|
|
return []string{constants.FileSharingExperiment}
|
|
}
|
|
|
|
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
|
|
func (f Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
|
|
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
switch ev.EventType {
|
|
case event.ManifestReceived:
|
|
log.Debugf("Manifest Received Event!: %v", ev)
|
|
handle := ev.Data[event.Handle]
|
|
fileKey := ev.Data[event.FileKey]
|
|
serializedManifest := ev.Data[event.SerializedManifest]
|
|
|
|
manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
|
|
if exists {
|
|
downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey))
|
|
if exists {
|
|
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
|
|
var manifest files.Manifest
|
|
err := json.Unmarshal([]byte(serializedManifest), &manifest)
|
|
|
|
if err == nil {
|
|
// We only need to check the file size here, as manifest is sent to engine and the file created
|
|
// will be bound to the size advertised in manifest.
|
|
fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey))
|
|
if fileSizeLimitExists {
|
|
fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, bits.UintSize)
|
|
if err == nil {
|
|
if manifest.FileSizeInBytes >= fileSizeLimit {
|
|
log.Errorf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue)
|
|
} else {
|
|
manifest.Title = manifest.FileName
|
|
manifest.FileName = downloadFilePath
|
|
log.Debugf("saving manifest")
|
|
err = manifest.Save(manifestFilePath)
|
|
if err != nil {
|
|
log.Errorf("could not save manifest: %v", err)
|
|
} else {
|
|
tempFile := ""
|
|
if runtime.GOOS == "android" {
|
|
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
|
|
log.Debugf("derived android temp path: %v", tempFile)
|
|
}
|
|
profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{
|
|
event.FileKey: fileKey,
|
|
event.Handle: handle,
|
|
event.SerializedManifest: string(manifest.Serialize()),
|
|
event.TempFile: tempFile,
|
|
event.NameSuggestion: manifest.Title,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.Errorf("error saving manifest: %v", err)
|
|
}
|
|
} else {
|
|
log.Errorf("found manifest path but not download path for %v", fileKey)
|
|
}
|
|
} else {
|
|
log.Errorf("no download path found for manifest: %v", fileKey)
|
|
}
|
|
case event.FileDownloaded:
|
|
fileKey := ev.Data[event.FileKey]
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
|
|
}
|
|
} else {
|
|
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
|
|
}
|
|
}
|
|
|
|
func (f Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
|
|
// nop
|
|
}
|
|
|
|
func (f Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
|
|
// Profile should not call us if FileSharing is disabled
|
|
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
scope, zone, zpath := path.GetScopeZonePath()
|
|
log.Debugf("file sharing contact receive value")
|
|
if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") {
|
|
fileKey := strings.Replace(zpath, ".manifest.size", "", 1)
|
|
size, err := strconv.Atoi(value)
|
|
// if size is valid and below the maximum size for a manifest
|
|
// this is to prevent malicious sharers from using large amounts of memory when distributing
|
|
// a manifest as we reconstruct this in-memory
|
|
if err == nil && size < files.MaxManifestSize {
|
|
profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle}))
|
|
} else {
|
|
profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle}))
|
|
}
|
|
}
|
|
} else {
|
|
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
|
|
}
|
|
}
|
|
|
|
// 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[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 {
|
|
Name string `json:"f"`
|
|
Hash string `json:"h"`
|
|
Nonce string `json:"n"`
|
|
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
|
|
}
|
|
|
|
func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string) {
|
|
if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists {
|
|
if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
|
|
log.Debugf("resuming %s", fileKey)
|
|
f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, files.MaxManifestSize*files.DefaultChunkSize)
|
|
} else {
|
|
log.Errorf("found manifest path but not download path for %s", fileKey)
|
|
}
|
|
} else {
|
|
log.Errorf("no stored manifest path found for %s", fileKey)
|
|
}
|
|
}
|
|
|
|
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) {
|
|
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
|
|
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
|
|
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
|
|
event.ProfileOnion: profile.GetOnion(),
|
|
event.FileKey: fileKey,
|
|
event.FilePath: path,
|
|
event.TempFile: "",
|
|
}))
|
|
} else {
|
|
log.Debugf("CheckDownloadStatus found .path but not .complete")
|
|
profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{
|
|
event.ProfileOnion: profile.GetOnion(),
|
|
event.FileKey: fileKey,
|
|
event.Progress: "-1",
|
|
event.FileSizeInChunks: "-1",
|
|
event.FilePath: path,
|
|
}))
|
|
}
|
|
}
|
|
|
|
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
|
|
fileKey, overlay, err := f.ShareFile(sharefilepath, profile)
|
|
if err != nil {
|
|
log.Errorf("error sharing file: %v", err)
|
|
} else if conversationID == -1 {
|
|
// FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume
|
|
// there is only one, and it is the custom profile image...
|
|
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey)
|
|
} else {
|
|
// Set a new attribute so we can associate this download with this conversation...
|
|
profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "")
|
|
id, err := profile.SendMessage(conversationID, overlay)
|
|
if err == nil {
|
|
return profile.EnhancedGetMessageById(conversationID, id)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process
|
|
// to downloadFilePath with a default filesize limit
|
|
func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error {
|
|
return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize)
|
|
}
|
|
|
|
// 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, limit uint64) error {
|
|
|
|
// assert that we are allowed to download the file
|
|
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
return errors.New("filesharing functionality is not enabled")
|
|
}
|
|
|
|
// Don't download files if the download or manifest path is not set
|
|
if downloadFilePath == "" || manifestFilePath == "" {
|
|
return errors.New("download path or manifest path is empty")
|
|
}
|
|
|
|
// We write to a temp file for Android...
|
|
if runtime.GOOS != "android" {
|
|
// Don't download files if the download file directory does not exist
|
|
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
|
|
return errors.New("download directory does not exist")
|
|
}
|
|
|
|
// Don't download files if the manifest file directory does not exist
|
|
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
|
|
return errors.New("manifest directory does not exist")
|
|
}
|
|
}
|
|
// 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)
|
|
|
|
// Store local.filesharing.filekey.limit as the max file size of the download
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.limit", key), strconv.FormatUint(limit, 10))
|
|
|
|
// Get the value of conversation.filesharing.filekey.manifest.size from `handle`
|
|
profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
|
|
|
|
return nil
|
|
}
|
|
|
|
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
|
|
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string) error {
|
|
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
|
|
if exists {
|
|
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
|
if err != nil || ts < time.Now().Unix()-2592000 {
|
|
log.Errorf("ignoring request to download a file offered more than 30 days ago")
|
|
return err
|
|
}
|
|
}
|
|
|
|
// set the filekey status to active
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
|
|
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
|
|
return nil
|
|
}
|
|
|
|
// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
|
|
func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error {
|
|
|
|
// assert that we are allowed to restart filesharing
|
|
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
return errors.New("filesharing functionality is not enabled")
|
|
}
|
|
|
|
// check that a manifest exists
|
|
manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
|
|
if manifestExists {
|
|
// everything is in order, so reshare this file with the engine
|
|
log.Debugf("restarting file share: %v", filekey)
|
|
return f.startFileShare(profile, filekey, manifest)
|
|
}
|
|
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
|
|
}
|
|
|
|
// ReShareFiles given a profile we iterate through all existing fileshares and re-share them
|
|
// if the time limit has not expired
|
|
func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
|
|
|
// assert that we are allowed to restart filesharing
|
|
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
return errors.New("filesharing functionality is not enabled")
|
|
}
|
|
|
|
keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, key := range keys {
|
|
// only look at timestamp keys
|
|
// this is an arbitrary choice
|
|
|
|
if strings.HasSuffix(key, ".ts") {
|
|
_, zonedpath := attr.ParseScope(key)
|
|
_, keypath := attr.ParseZone(zonedpath)
|
|
keyparts := strings.Split(keypath, ".")
|
|
|
|
// assert that the key is well-formed
|
|
if len(keyparts) == 3 && keyparts[2] == "ts" {
|
|
// fetch the timestamp key
|
|
filekey := strings.Join(keyparts[:2], ".")
|
|
sharedFile, err := f.GetFileShareInfo(profile, filekey)
|
|
|
|
// If we haven't explicitly stopped sharing the file AND
|
|
// If fewer than 30 days have passed since we originally shared this file,
|
|
// Then attempt to share this file again...
|
|
// TODO: In the future this would be the point to change the timestamp and reshare the file...
|
|
if err == nil && sharedFile.Active {
|
|
err := f.RestartFileShare(profile, filekey)
|
|
if err != nil {
|
|
log.Errorf("could not reshare file: %v", err)
|
|
}
|
|
} else {
|
|
log.Errorf("could not get fileshare info %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetFileShareInfo returns information related to a known fileshare.
|
|
// An error is returned if the data is incomplete
|
|
func (f *Functionality) GetFileShareInfo(profile peer.CwtchPeer, filekey string) (*SharedFile, error) {
|
|
timestampString, tsExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
|
|
pathString, pathExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", filekey))
|
|
activeString, activeExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey))
|
|
if tsExists && pathExists && activeExists {
|
|
timestamp, err := strconv.Atoi(timestampString)
|
|
if err == nil {
|
|
|
|
dateShared := time.Unix(int64(timestamp), 0)
|
|
expired := time.Since(dateShared) >= time.Hour*24*30
|
|
|
|
return &SharedFile{
|
|
FileKey: filekey,
|
|
Path: pathString,
|
|
DateShared: dateShared,
|
|
Active: !expired && activeString == constants.True,
|
|
Expired: expired,
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("nonexistant or malformed fileshare %v", filekey)
|
|
}
|
|
|
|
// 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) (string, string, error) {
|
|
|
|
// assert that we are allowed to share files
|
|
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
|
return "", "", errors.New("filesharing functionality is not enabled")
|
|
}
|
|
|
|
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))
|
|
|
|
// the sender needs to know the location of the file so they can display it in a preview...
|
|
// This eventually becomes a message attribute, but we don't have access to the message identifier until
|
|
// the message gets sent.
|
|
// In the worst case, this can be obtained using CheckDownloadStatus (though in practice this lookup will be
|
|
// rare because the UI will almost always initiate the construction of a preview a file directly after sending it).
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), filepath)
|
|
|
|
// Store the timestamp, manifest and manifest size for later.
|
|
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)))))
|
|
|
|
err = f.startFileShare(profile, key, string(serializedManifest))
|
|
|
|
return key, string(wrapperJSON), err
|
|
}
|
|
|
|
// SharedFile encapsulates information about a shared file
|
|
// including the file key, file path, the original share date and the
|
|
// current sharing status
|
|
type SharedFile struct {
|
|
|
|
// The roothash.nonce identifier derived for this file share
|
|
FileKey string
|
|
|
|
// Path is the OS specific location of the file
|
|
Path string
|
|
|
|
// DateShared is the original datetime the file was shared
|
|
DateShared time.Time
|
|
|
|
// Active is true if the file is currently being shared, false otherwise
|
|
Active bool
|
|
|
|
// Expired is true if the file is not eligible to be shared (because e.g. it has been too long since the file was originally shared,
|
|
// or the file no longer exists).
|
|
Expired bool
|
|
}
|
|
|
|
func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string {
|
|
data, err := json.Marshal(f.GetSharedFiles(profile, conversationID))
|
|
if err == nil {
|
|
return string(data)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetSharedFiles returns all file shares associated with a given conversation
|
|
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
|
|
sharedFiles := []SharedFile{}
|
|
ci, err := profile.GetConversationInfo(conversationID)
|
|
if err == nil {
|
|
for k := range ci.Attributes {
|
|
// when we share a file with a conversation we set a single attribute conversation.filesharing.<filekey>
|
|
if strings.HasPrefix(k, "conversation.filesharing") {
|
|
parts := strings.SplitN(k, ".", 3)
|
|
if len(parts) == 3 {
|
|
key := parts[2]
|
|
sharedFile, err := f.GetFileShareInfo(profile, key)
|
|
if err == nil {
|
|
sharedFiles = append(sharedFiles, *sharedFile)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sharedFiles
|
|
}
|
|
|
|
// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem
|
|
func GenerateDownloadPath(basePath, fileName string, overwrite bool) (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])
|
|
}
|
|
|
|
if !overwrite {
|
|
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)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
|
|
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) {
|
|
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
|
// set the filekey status to inactive
|
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
|
|
profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
|
|
}
|
|
|
|
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
|
|
func (f *Functionality) StopAllFileShares(profile peer.CwtchPeer) {
|
|
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
|
profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
|
|
}
|