2021-09-30 00:57:13 +00:00
package filesharing
import (
"crypto/rand"
2023-01-05 21:52:43 +00:00
"cwtch.im/cwtch/event"
2021-09-30 00:57:13 +00:00
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math"
2023-01-05 21:52:43 +00:00
"math/bits"
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"
2022-12-12 21:17:39 +00:00
"runtime"
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 {
}
2023-01-25 20:34:58 +00:00
func ( f Functionality ) EventsToRegister ( ) [ ] event . Type {
2023-01-05 21:52:43 +00:00
return [ ] event . Type { event . ManifestReceived , event . FileDownloaded }
}
2023-01-25 20:34:58 +00:00
func ( f Functionality ) ExperimentsToRegister ( ) [ ] string {
2023-01-05 21:52:43 +00:00
return [ ] string { constants . FileSharingExperiment }
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func ( f Functionality ) OnEvent ( ev event . Event , profile peer . CwtchPeer ) {
2023-01-25 20:32:26 +00:00
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 ) )
2023-01-05 21:52:43 +00:00
if exists {
2023-01-25 20:32:26 +00:00
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 )
2023-01-05 21:52:43 +00:00
} else {
2023-01-25 20:32:26 +00:00
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 ,
} ) )
2023-01-05 21:52:43 +00:00
}
}
}
}
2023-01-25 20:32:26 +00:00
} else {
log . Errorf ( "error saving manifest: %v" , err )
2023-01-05 21:52:43 +00:00
}
} else {
2023-01-25 20:32:26 +00:00
log . Errorf ( "found manifest path but not download path for %v" , fileKey )
2023-01-05 21:52:43 +00:00
}
} else {
2023-01-25 20:32:26 +00:00
log . Errorf ( "no download path found for manifest: %v" , fileKey )
2023-01-05 21:52:43 +00:00
}
2023-01-25 20:32:26 +00:00
case event . FileDownloaded :
fileKey := ev . Data [ event . FileKey ]
profile . SetScopedZonedAttribute ( attr . LocalScope , attr . FilesharingZone , fmt . Sprintf ( "%s.complete" , fileKey ) , "true" )
2023-01-05 21:52:43 +00:00
}
2023-01-25 20:32:26 +00:00
} else {
log . Errorf ( "profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error." )
2023-01-05 21:52:43 +00:00
}
}
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 ) {
2023-01-25 20:32:26 +00:00
// 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 } ) )
}
2023-01-05 21:52:43 +00:00
}
2023-01-25 20:32:26 +00:00
} else {
log . Errorf ( "profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error." )
2023-01-05 21:52:43 +00:00
}
}
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
}
2023-02-21 23:55:14 +00:00
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 {
2023-02-28 18:00:32 +00:00
log . Debugf ( "resuming %s" , fileKey )
2023-02-21 23:55:14 +00:00
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 {
2023-02-28 18:00:32 +00:00
log . Debugf ( "CheckDownloadStatus found .path but not .complete" )
2023-02-21 23:55:14 +00:00
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 )
}
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
2022-08-19 16:27:19 +00:00
func ( f * Functionality ) DownloadFile ( profile peer . CwtchPeer , conversationID int , downloadFilePath string , manifestFilePath string , key string , limit uint64 ) error {
2023-01-25 20:32:26 +00:00
// assert that we are allowed to download the file
if ! profile . IsFeatureEnabled ( constants . FileSharingExperiment ) {
return errors . New ( "filesharing functionality is not enabled" )
}
2022-08-19 16:27:19 +00:00
// 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" )
}
2022-12-12 21:17:39 +00:00
// 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" )
}
2022-08-19 16:27:19 +00:00
2022-12-12 21:17:39 +00:00
// 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" )
}
2022-08-19 16:27:19 +00:00
}
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
2022-02-04 00:07:49 +00:00
// Store local.filesharing.filekey.limit as the max file size of the download
2022-02-04 21:19:47 +00:00
profile . SetScopedZonedAttribute ( attr . LocalScope , attr . FilesharingZone , fmt . Sprintf ( "%s.limit" , key ) , strconv . FormatUint ( limit , 10 ) )
2022-02-04 00:07:49 +00:00
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 ) )
2022-08-19 16:27:19 +00:00
return nil
2021-09-30 00:57:13 +00:00
}
2023-01-05 21:52:43 +00:00
// 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
}
2022-07-06 18:06:06 +00:00
// 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 {
2023-01-25 20:32:26 +00:00
// assert that we are allowed to restart filesharing
if ! profile . IsFeatureEnabled ( constants . FileSharingExperiment ) {
return errors . New ( "filesharing functionality is not enabled" )
}
2022-07-06 18:06:06 +00:00
// 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
2023-01-05 19:21:08 +00:00
log . Debugf ( "restarting file share: %v" , filekey )
2023-01-05 21:52:43 +00:00
return f . startFileShare ( profile , filekey , manifest )
2022-07-06 18:06:06 +00:00
}
return fmt . Errorf ( "manifest does not exist for filekey: %v" , filekey )
}
2022-07-05 22:31:44 +00:00
// 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 {
2023-01-25 20:32:26 +00:00
// assert that we are allowed to restart filesharing
if ! profile . IsFeatureEnabled ( constants . FileSharingExperiment ) {
return errors . New ( "filesharing functionality is not enabled" )
}
2022-07-06 03:41:16 +00:00
keys , err := profile . GetScopedZonedAttributeKeys ( attr . LocalScope , attr . FilesharingZone )
2022-07-06 03:29:07 +00:00
if err != nil {
return err
}
for _ , key := range keys {
// only look at timestamp keys
// this is an arbitrary choice
2023-01-05 19:21:08 +00:00
2022-07-06 03:29:07 +00:00
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 ] , "." )
2022-07-06 18:06:06 +00:00
sharedFile , err := f . GetFileShareInfo ( profile , filekey )
2022-07-06 03:29:07 +00:00
2022-07-06 18:06:06 +00:00
// If we haven't explicitly stopped sharing the file AND
2022-07-06 03:29:07 +00:00
// If fewer than 30 days have passed since we originally shared this file,
2022-07-06 18:06:06 +00:00
// Then attempt to share this file again...
2022-07-06 03:29:07 +00:00
// TODO: In the future this would be the point to change the timestamp and reshare the file...
2022-07-06 18:06:06 +00:00
if err == nil && sharedFile . Active {
2023-01-05 19:21:08 +00:00
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 )
2022-07-05 22:31:44 +00:00
}
}
}
}
2022-07-06 03:29:07 +00:00
return nil
2022-07-05 22:31:44 +00:00
}
2022-07-06 18:06:06 +00:00
// 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 )
}
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
2022-02-04 00:07:49 +00:00
func ( f * Functionality ) ShareFile ( filepath string , profile peer . CwtchPeer ) ( string , string , error ) {
2023-01-25 20:32:26 +00:00
// assert that we are allowed to share files
if ! profile . IsFeatureEnabled ( constants . FileSharingExperiment ) {
return "" , "" , errors . New ( "filesharing functionality is not enabled" )
}
2021-09-30 00:57:13 +00:00
manifest , err := files . CreateManifest ( filepath )
if err != nil {
2022-02-04 00:07:49 +00:00
return "" , "" , err
2021-09-30 00:57:13 +00:00
}
var nonce [ 24 ] byte
if _ , err := io . ReadFull ( rand . Reader , nonce [ : ] ) ; err != nil {
log . Errorf ( "Cannot read from random: %v\n" , err )
2022-02-04 00:07:49 +00:00
return "" , "" , err
2021-09-30 00:57:13 +00:00
}
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
2022-01-20 21:21:57 +00:00
// 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.
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
2023-01-05 21:52:43 +00:00
err = f . startFileShare ( profile , key , string ( serializedManifest ) )
2021-09-30 00:57:13 +00:00
2022-02-04 00:07:49 +00:00
return key , string ( wrapperJSON ) , err
2021-09-30 00:57:13 +00:00
}
2021-12-14 21:21:45 +00:00
2022-07-06 18:06:06 +00:00
// 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
}
2023-02-21 23:55:14 +00:00
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 ""
}
2022-07-06 18:06:06 +00:00
// 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
}
2021-12-19 00:34:07 +00:00
// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem
2022-02-03 22:39:52 +00:00
func GenerateDownloadPath ( basePath , fileName string , overwrite bool ) ( 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 ] )
}
2022-02-03 22:39:52 +00:00
if ! overwrite {
for i := 2 ; ; i ++ {
if _ , err := os . Open ( filePath ) ; os . IsNotExist ( err ) {
if _ , err := os . Open ( manifestPath ) ; os . IsNotExist ( err ) {
return
}
2021-12-14 21:21:45 +00:00
}
2022-02-03 22:39:52 +00:00
filePath = fmt . Sprintf ( "%s%s (%d)%s" , basePath , fileNameBase , i , fileNameExt )
manifestPath = fmt . Sprintf ( "%s.manifest" , filePath )
2021-12-14 21:21:45 +00:00
}
}
2022-02-03 22:39:52 +00:00
return
2021-12-14 21:21:45 +00:00
}
2023-01-05 21:52:43 +00:00
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
func ( f * Functionality ) StopFileShare ( profile peer . CwtchPeer , fileKey string ) {
2023-01-25 20:32:26 +00:00
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
2023-01-05 21:52:43 +00:00
// 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 ) {
2023-01-25 20:32:26 +00:00
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
2023-01-05 21:52:43 +00:00
profile . PublishEvent ( event . NewEvent ( event . StopAllFileShares , map [ event . Field ] string { } ) )
}