Skip to content
Snippets Groups Projects
Commit 0fa88a04 authored by Jake Taylor's avatar Jake Taylor :lips:
Browse files

Merge branch 'XX-4588/FtEmImpl' into 'project/fileUpload'

fileTransfer eventmodel implementation for channels

See merge request !95
parents 24fb185a 4ad50a9b
No related branches found
No related tags found
3 merge requests!95fileTransfer eventmodel implementation for channels,!71Project/file upload,!67fix for latest client release
......@@ -22,9 +22,11 @@ import (
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/v4/channels"
cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
"gitlab.com/elixxir/client/v4/cmix/rounds"
cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
cryptoChannel "gitlab.com/elixxir/crypto/channel"
"gitlab.com/elixxir/crypto/fileTransfer"
"gitlab.com/elixxir/crypto/message"
"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
......@@ -45,6 +47,131 @@ type wasmModel struct {
mutedUserCB wChannels.MutedUserCallback
}
// ReceiveFile is called when a file upload or download beings.
//
// fileLink and fileData are nillable and may be updated based
// upon the UUID or file ID later.
//
// fileID is always unique to the fileData. fileLink is the JSON of
// channelsFileTransfer.FileLink.
//
// Returns any fatal errors.
func (w *wasmModel) ReceiveFile(fileID fileTransfer.ID, fileLink,
fileData []byte, timestamp time.Time, status cft.Status) error {
newFile := &File{
Id: fileID.Marshal(),
Data: fileData,
Link: fileLink,
Timestamp: timestamp,
Status: uint8(status),
}
return w.upsertFile(newFile)
}
// UpdateFile is called when a file upload or download completes or changes.
//
// fileLink, fileData, timestamp, and status are all nillable and may be
// updated based upon the file ID at a later date. If a nil value is passed,
// then make no update.
//
// Returns an error if the file cannot be updated. It must return
// channels.NoMessageErr if the file does not exist.
func (w *wasmModel) UpdateFile(fileID fileTransfer.ID, fileLink,
fileData []byte, timestamp *time.Time, status *cft.Status) error {
parentErr := "[Channels indexedDB] failed to UpdateFile"
// Get the File as it currently exists in storage
fileObj, err := impl.Get(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
if err != nil {
if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
return errors.WithMessage(channels.NoMessageErr, parentErr)
}
return errors.WithMessage(err, parentErr)
}
currentFile, err := valueToFile(fileObj)
if err != nil {
return errors.WithMessage(err, parentErr)
}
// Update the fields if specified
if status != nil {
currentFile.Status = uint8(*status)
}
if timestamp != nil {
currentFile.Timestamp = *timestamp
}
if fileData != nil {
currentFile.Data = fileData
}
if fileLink != nil {
currentFile.Link = fileLink
}
return w.upsertFile(currentFile)
}
// upsertFile is a helper function that will update an existing File
// if File.Id is specified. Otherwise, it will perform an insert.
func (w *wasmModel) upsertFile(newFile *File) error {
newFileJson, err := json.Marshal(&newFile)
if err != nil {
return err
}
fileObj, err := utils.JsonToJS(newFileJson)
if err != nil {
return err
}
_, err = impl.Put(w.db, fileStoreName, fileObj)
return err
}
// GetFile returns the ModelFile containing the file data and download link
// for the given file ID.
//
// Returns an error if the file cannot be retrieved. It must return
// channels.NoMessageErr if the file does not exist.
func (w *wasmModel) GetFile(fileID fileTransfer.ID) (
cft.ModelFile, error) {
fileObj, err := impl.Get(w.db, fileStoreName,
impl.EncodeBytes(fileID.Marshal()))
if err != nil {
if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
return cft.ModelFile{}, channels.NoMessageErr
}
return cft.ModelFile{}, err
}
resultFile, err := valueToFile(fileObj)
if err != nil {
return cft.ModelFile{}, err
}
result := cft.ModelFile{
ID: fileTransfer.NewID(resultFile.Data),
Link: resultFile.Link,
Data: resultFile.Data,
Timestamp: resultFile.Timestamp,
Status: cft.Status(resultFile.Status),
}
return result, nil
}
// DeleteFile deletes the file with the given file ID.
//
// Returns fatal errors. It must return channels.NoMessageErr if the file
// does not exist.
func (w *wasmModel) DeleteFile(fileID fileTransfer.ID) error {
err := impl.Delete(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
if err != nil {
if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
return channels.NoMessageErr
}
}
return err
}
// JoinChannel is called whenever a channel is joined locally.
func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
parentErr := errors.New("failed to JoinChannel")
......@@ -70,7 +197,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
return
}
_, err = impl.Put(w.db, channelsStoreName, channelObj)
_, err = impl.Put(w.db, channelStoreName, channelObj)
if err != nil {
jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
"Unable to put Channel: %+v", err))
......@@ -82,7 +209,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
parentErr := errors.New("failed to LeaveChannel")
// Delete the channel from storage
err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String()))
err := impl.Delete(w.db, channelStoreName, js.ValueOf(channelID.String()))
if err != nil {
jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
"Unable to delete Channel: %+v", err))
......@@ -505,9 +632,11 @@ func (w *wasmModel) MuteUser(
// valueToMessage is a helper for converting js.Value to Message.
func valueToMessage(msgObj js.Value) (*Message, error) {
resultMsg := &Message{}
err := json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
if err != nil {
return nil, err
}
return resultMsg, nil
return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
}
// valueToFile is a helper for converting js.Value to File.
func valueToFile(fileObj js.Value) (*File, error) {
resultFile := &File{}
return resultFile, json.Unmarshal([]byte(utils.JsToJson(fileObj)), resultFile)
}
......@@ -10,9 +10,13 @@
package main
import (
"bytes"
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
"gitlab.com/elixxir/crypto/fileTransfer"
"os"
"strconv"
"testing"
......@@ -42,6 +46,79 @@ func dummyReceivedMessageCB(uint64, *id.ID, bool) {}
func dummyDeletedMessageCB(message.ID) {}
func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
// Happy path test for receiving, updating, getting, and deleting a File.
func TestWasmModel_ReceiveFile(t *testing.T) {
testString := "TestWasmModel_ReceiveFile"
m, err := newWASMModel(testString, nil,
dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
if err != nil {
t.Fatal(err)
}
testTs := time.Now()
testBytes := []byte(testString)
testStatus := cft.Downloading
// Insert a test row
fId := fileTransfer.NewID(testBytes)
err = m.ReceiveFile(fId, testBytes, testBytes, testTs, testStatus)
if err != nil {
t.Fatal(err)
}
// Attempt to get stored row
storedFile, err := m.GetFile(fId)
if err != nil {
t.Fatal(err)
}
// Spot check stored attribute
if !bytes.Equal(storedFile.Link, testBytes) {
t.Fatalf("Got unequal FileLink values")
}
// Attempt to updated stored row
newTs := time.Now()
newBytes := []byte("test")
newStatus := cft.Complete
err = m.UpdateFile(fId, nil, newBytes, &newTs, &newStatus)
if err != nil {
t.Fatal(err)
}
// Check that the update took
updatedFile, err := m.GetFile(fId)
if err != nil {
t.Fatal(err)
}
// Link should not have changed
if !bytes.Equal(updatedFile.Link, testBytes) {
t.Fatalf("Link should not have changed")
}
// Other attributes should have changed
if !bytes.Equal(updatedFile.Data, newBytes) {
t.Fatalf("Data should have updated")
}
if !updatedFile.Timestamp.Equal(newTs) {
t.Fatalf("TS should have updated, expected %s got %s",
newTs, updatedFile.Timestamp)
}
if updatedFile.Status != newStatus {
t.Fatalf("Status should have updated")
}
// Delete the row
err = m.DeleteFile(fId)
if err != nil {
t.Fatal(err)
}
// Check that the delete operation took and get provides the expected error
_, err = m.GetFile(fId)
if err == nil || !errors.Is(channels.NoMessageErr, err) {
t.Fatal(err)
}
}
// Happy path, insert message and look it up
func TestWasmModel_GetMessage(t *testing.T) {
cipher, err := cryptoChannel.NewCipher(
......@@ -54,7 +131,7 @@ func TestWasmModel_GetMessage(t *testing.T) {
if c != nil {
cs = "_withCipher"
}
testString := "TestWasmModel_msgIDLookup" + cs
testString := "TestWasmModel_GetMessage" + cs
t.Run(testString, func(t *testing.T) {
storage.GetLocalStorage().Clear()
testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
......@@ -235,7 +312,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
}
eventModel.JoinChannel(testChannel)
eventModel.JoinChannel(testChannel2)
results, err2 := impl.Dump(eventModel.db, channelsStoreName)
results, err2 := impl.Dump(eventModel.db, channelStoreName)
if err2 != nil {
t.Fatal(err2)
}
......@@ -243,7 +320,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
t.Fatalf("Expected 2 channels to exist")
}
eventModel.LeaveChannel(testChannel.ReceptionID)
results, err = impl.Dump(eventModel.db, channelsStoreName)
results, err = impl.Dump(eventModel.db, channelStoreName)
if err != nil {
t.Fatal(err)
}
......
......@@ -23,7 +23,7 @@ import (
// currentVersion is the current version of the IndexedDb runtime. Used for
// migration purposes.
const currentVersion uint = 1
const currentVersion uint = 2
// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
// The name should be a base64 encoding of the users public key. Returns the
......@@ -62,6 +62,14 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
oldVersion = 1
}
if oldVersion == 1 && newVersion >= 2 {
err := v2Upgrade(db)
if err != nil {
return err
}
oldVersion = 2
}
// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
return nil
})
......@@ -136,10 +144,22 @@ func v1Upgrade(db *idb.Database) error {
}
// Build Channel ObjectStore
_, err = db.CreateObjectStore(channelsStoreName, storeOpts)
_, err = db.CreateObjectStore(channelStoreName, storeOpts)
if err != nil {
return err
}
return nil
}
// v1Upgrade performs the v1 -> v2 database upgrade.
//
// This can never be changed without permanently breaking backwards
// compatibility.
func v2Upgrade(db *idb.Database) error {
_, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{
KeyPath: js.ValueOf(pkeyName),
AutoIncrement: false,
})
return err
}
......@@ -18,8 +18,9 @@ const (
pkeyName = "id"
// Text representation of the names of the various [idb.ObjectStore].
messageStoreName = "messages"
channelsStoreName = "channels"
messageStoreName = "messages"
channelStoreName = "channels"
fileStoreName = "files"
// Message index names.
messageStoreMessageIndex = "message_id_index"
......@@ -73,3 +74,21 @@ type Channel struct {
Name string `json:"name"`
Description string `json:"description"`
}
// File defines the IndexedDb representation of a single File.
type File struct {
// Id is a unique identifier for a given File.
Id []byte `json:"id"` // Matches pkeyName
// Data stores the actual contents of the File.
Data []byte `json:"data"`
// Link contains all the information needed to download the file data.
Link []byte `json:"link"`
// Timestamp is the last time the file data, link, or status was modified.
Timestamp time.Time `json:"timestamp"`
// Status of the file in the event model.
Status uint8 `json:"status"`
}
......@@ -40,6 +40,20 @@ func TestGetIndex_NoMessageError(t *testing.T) {
}
}
// Test simple put on empty DB is successful
func TestPut(t *testing.T) {
objectStoreName := "messages"
db := newTestDB(objectStoreName, "index", t)
testValue := js.ValueOf(make(map[string]interface{}))
result, err := Put(db, objectStoreName, testValue)
if err != nil {
t.Fatalf(err.Error())
}
if !result.Equal(js.ValueOf(1)) {
t.Fatalf("Failed to generate autoincremented key")
}
}
// newTestDB creates a new idb.Database for testing.
func newTestDB(name, index string, t *testing.T) *idb.Database {
// Attempt to open database object
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment