Adds attachment fetching/uploading
This commit is contained in:
57
model/attachment.go
Normal file
57
model/attachment.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AttachmentStore struct {
|
||||||
|
basePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAttachmentStore(basePath string) AttachmentStore {
|
||||||
|
_, err := os.Stat(basePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(basePath, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttachmentStore{
|
||||||
|
basePath: basePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AttachmentStore) FetchAttachment(guid string) (io.Reader, error) {
|
||||||
|
fullPath := path.Join(s.basePath, guid)
|
||||||
|
f, err := os.Open(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bufio.NewReader(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AttachmentStore) StoreAttachment(filename string, reader io.Reader) (*string, error) {
|
||||||
|
// Generate GUID
|
||||||
|
guid := uuid.New().String()
|
||||||
|
|
||||||
|
fullPath := path.Join(s.basePath, guid)
|
||||||
|
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := bufio.NewReader(reader)
|
||||||
|
w := bufio.NewWriter(f)
|
||||||
|
_, err = w.ReadFrom(r)
|
||||||
|
|
||||||
|
return &guid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AttachmentStore) DeleteAttachment(guid string) error {
|
||||||
|
fullPath := path.Join(s.basePath, guid)
|
||||||
|
return os.Remove(fullPath)
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"code.severnaya.net/kordophone-mock/v2/data"
|
"code.severnaya.net/kordophone-mock/v2/data"
|
||||||
@@ -16,13 +19,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
version string
|
version string
|
||||||
conversations []model.Conversation
|
conversations []model.Conversation
|
||||||
authTokens []model.AuthToken
|
authTokens []model.AuthToken
|
||||||
messageStore map[string][]model.Message
|
attachmentStore model.AttachmentStore
|
||||||
updateItems map[int]model.UpdateItem
|
messageStore map[string][]model.Message
|
||||||
updateChannels []chan []model.UpdateItem
|
updateItems map[int]model.UpdateItem
|
||||||
updateItemSeq int
|
updateChannels []chan []model.UpdateItem
|
||||||
|
updateItemSeq int
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessagesQuery struct {
|
type MessagesQuery struct {
|
||||||
@@ -50,14 +54,17 @@ func (e *DatabaseError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
|
attachmentStorePath := path.Join(os.TempDir(), "kpmock", "attachments")
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
conversations: []model.Conversation{},
|
conversations: []model.Conversation{},
|
||||||
authTokens: []model.AuthToken{},
|
authTokens: []model.AuthToken{},
|
||||||
messageStore: make(map[string][]model.Message),
|
attachmentStore: model.NewAttachmentStore(attachmentStorePath),
|
||||||
updateItems: make(map[int]model.UpdateItem),
|
messageStore: make(map[string][]model.Message),
|
||||||
updateChannels: []chan []model.UpdateItem{},
|
updateItems: make(map[int]model.UpdateItem),
|
||||||
updateItemSeq: 0,
|
updateChannels: []chan []model.UpdateItem{},
|
||||||
|
updateItemSeq: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +294,18 @@ func (s *Server) MarkConversationAsRead(conversation *model.Conversation) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) FetchAttachment(guid string) (io.Reader, error) {
|
||||||
|
return s.attachmentStore.FetchAttachment(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) UploadAttachment(filename string, reader io.Reader) (*string, error) {
|
||||||
|
return s.attachmentStore.StoreAttachment(filename, reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) DeleteAttachment(guid string) error {
|
||||||
|
return s.attachmentStore.DeleteAttachment(guid)
|
||||||
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
func (s *Server) registerAuthToken(token *model.AuthToken) {
|
func (s *Server) registerAuthToken(token *model.AuthToken) {
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
|
type UploadAttachmentResponse struct {
|
||||||
|
TransferGUID string `json:"fileTransferGUID"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -330,6 +331,67 @@ func (m *MockHTTPServer) handleMarkConversation(w http.ResponseWriter, r *http.R
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockHTTPServer) handleFetchAttachment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !m.requireAuthentication(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guid := r.URL.Query().Get("guid")
|
||||||
|
if guid == "" {
|
||||||
|
log.Error().Msg("fetchAttachment: Missing 'guid' parameter")
|
||||||
|
http.Error(w, "no guid parameter specified", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := m.Server.FetchAttachment(guid)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("fetchAttachment: Could not load attachment from store: %s", err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dw := bufio.NewWriter(w)
|
||||||
|
_, err = dw.ReadFrom(reader)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("fetchAttachment: Error reading attachment data: %s", err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockHTTPServer) handleUploadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !m.requireAuthentication(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := r.URL.Query().Get("filename")
|
||||||
|
if filename == "" {
|
||||||
|
log.Error().Msgf("uploadAttachment: filename not provided")
|
||||||
|
http.Error(w, "filename not provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guid, err := m.Server.UploadAttachment(filename, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("uploadAttachment: error storing attachment: %s", err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := UploadAttachmentResponse{
|
||||||
|
TransferGUID: *guid,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockHTTPServer) handleUpdates(w http.ResponseWriter, r *http.Request) {
|
func (m *MockHTTPServer) handleUpdates(w http.ResponseWriter, r *http.Request) {
|
||||||
if !m.requireAuthentication(w, r) {
|
if !m.requireAuthentication(w, r) {
|
||||||
return
|
return
|
||||||
@@ -380,6 +442,8 @@ func NewMockHTTPServer(config MockHTTPServerConfiguration) *MockHTTPServer {
|
|||||||
this.mux.Handle("/pollUpdates", http.HandlerFunc(this.handlePollUpdates))
|
this.mux.Handle("/pollUpdates", http.HandlerFunc(this.handlePollUpdates))
|
||||||
this.mux.Handle("/sendMessage", http.HandlerFunc(this.handleSendMessage))
|
this.mux.Handle("/sendMessage", http.HandlerFunc(this.handleSendMessage))
|
||||||
this.mux.Handle("/markConversation", http.HandlerFunc(this.handleMarkConversation))
|
this.mux.Handle("/markConversation", http.HandlerFunc(this.handleMarkConversation))
|
||||||
|
this.mux.Handle("/fetchAttachment", http.HandlerFunc(this.handleFetchAttachment))
|
||||||
|
this.mux.Handle("/uploadAttachment", http.HandlerFunc(this.handleUploadAttachment))
|
||||||
this.mux.Handle("/updates", http.HandlerFunc(this.handleUpdates))
|
this.mux.Handle("/updates", http.HandlerFunc(this.handleUpdates))
|
||||||
|
|
||||||
this.mux.Handle("/", http.HandlerFunc(this.handleNotFound))
|
this.mux.Handle("/", http.HandlerFunc(this.handleNotFound))
|
||||||
|
|||||||
@@ -602,3 +602,47 @@ func TestSendMessage(t *testing.T) {
|
|||||||
t.Fatalf("Message not found in conversation")
|
t.Fatalf("Message not found in conversation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAttachments(t *testing.T) {
|
||||||
|
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||||
|
httpServer := httptest.NewServer(s)
|
||||||
|
|
||||||
|
// Send upload request
|
||||||
|
testData := "hello world!"
|
||||||
|
attachmentDataReader := strings.NewReader(testData)
|
||||||
|
resp, err := http.Post(httpServer.URL+"/uploadAttachment?filename=test.txt", "application/data", attachmentDataReader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error uploading attachment: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error decoding body: %s", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response web.UploadAttachmentResponse
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error decoding response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guid := response.TransferGUID
|
||||||
|
|
||||||
|
// Cleanup after ourselves
|
||||||
|
defer s.Server.DeleteAttachment(guid)
|
||||||
|
|
||||||
|
// Fetch it back
|
||||||
|
resp, err = http.Get(httpServer.URL + fmt.Sprintf("/fetchAttachment?guid=%s", guid))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error fetching attachment: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading attachment data: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(responseData) != testData {
|
||||||
|
t.Fatalf("Didn't get expected response data: %s (got %s)", testData, responseData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user