diff --git a/docker/minIO/docker-compose.yml b/docker/minIO/docker-compose.yml
index a5c8fa1..4f6b6c5 100644
--- a/docker/minIO/docker-compose.yml
+++ b/docker/minIO/docker-compose.yml
@@ -2,7 +2,7 @@ version: "3.8"
services:
minio:
- image: minio/minio:RELEASE.2024-03-19T00-00-00Z
+ image: minio/minio:latest
container_name: minio
restart: unless-stopped
environment:
@@ -14,4 +14,4 @@ services:
ports:
- "9000:9000"
- "9001:9001"
- command: server /data --console-address ":9001"
\ No newline at end of file
+ command: server /data --console-address ":9001"
diff --git a/docker/postgres/docker-compose.yml b/docker/postgres/docker-compose.yml
index 1ffdffe..f4a5a19 100644
--- a/docker/postgres/docker-compose.yml
+++ b/docker/postgres/docker-compose.yml
@@ -7,4 +7,4 @@ services:
ports:
- "5432:5432"
volumes:
- - ./data:/var/lib/postgresql/dat
+ - ./data:/var/lib/postgresql/data
diff --git a/go-socket b/go-socket
index d6c0a7c..0635fe1 100755
Binary files a/go-socket and b/go-socket differ
diff --git a/machine-client/index.html b/machine-client/index.html
index d62ffba..9a962e3 100644
--- a/machine-client/index.html
+++ b/machine-client/index.html
@@ -86,6 +86,8 @@
+
+
@@ -260,9 +262,10 @@
fields: [
{ id: 'mu-token', label: 'token', ph: '' },
{ id: 'mu-connectionid', label: 'connectionid', ph: 'UUID' },
- { id: 'mu-msgContent', label: 'msgContent', ph: 'message text' },
+ { id: 'mu-msgContent', label: 'msgContent', ph: 'message text (optional if mediaKey set)' },
+ { id: 'mu-attachedFile', label: 'attachedFile', ph: 'key returned by /new/file (optional)' },
],
- submit: () => httpPost('/msg/user', { token:'mu-token', connectionid:'mu-connectionid', msgContent:'mu-msgContent' })
+ submit: () => httpPost('/msg/user', { token:'mu-token', connectionid:'mu-connectionid', msgContent:'mu-msgContent', attachedFile:'mu-attachedFile' })
},
'msg-group': {
title: 'POST /msg/group — send message to group',
@@ -273,6 +276,28 @@
],
submit: () => httpPost('/msg/group', { token:'mg-token', groupid:'mg-groupid', content:'mg-content' })
},
+ 'file-upload': {
+ title: 'POST /new/file — upload file (multipart, token in header)',
+ renderCustom: () => `
+
+
+
+
+
+
+ `
+ },
+ 'file-download': {
+ title: 'POST /get/file — get presigned download URL (token in header)',
+ renderCustom: () => `
+
+
+
+
+
+
+ `
+ },
'websocket': {
title: 'WS /ws — WebSocket connection',
renderCustom: () => {
@@ -324,6 +349,7 @@
if (def.renderCustom) {
fieldsEl.innerHTML = def.renderCustom();
+ autofillTokens();
} else {
let html = '';
for (const f of def.fields) {
@@ -420,6 +446,45 @@
log('WS →', msg, 'log-info');
}
+ async function submitFileUpload() {
+ const token = document.getElementById('fu-token').value;
+ const connectionid = document.getElementById('fu-connectionid').value;
+ const fileInput = document.getElementById('fu-file');
+ if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; }
+ const form = new FormData();
+ form.append('connectionid', connectionid);
+ form.append('file', fileInput.files[0]);
+ log('HTTP /new/file', `→ connectionid=${connectionid} file=${fileInput.files[0].name}`, 'log-info');
+ try {
+ const resp = await fetch(baseUrl() + '/new/file', { method: 'POST', headers: { token }, body: form });
+ const text = await resp.text();
+ log(`HTTP ${resp.status}`, text, resp.ok ? 'log-http' : 'log-err');
+ if (resp.ok) {
+ const keyEl = document.getElementById('fd-key');
+ if (keyEl) keyEl.value = text;
+ const dmKeyEl = document.getElementById('mu-attachedFile');
+ if (dmKeyEl) dmKeyEl.value = text;
+ }
+ } catch(e) {
+ log('HTTP ERR', e.message, 'log-err');
+ }
+ }
+
+ async function submitFileDownload() {
+ const token = document.getElementById('fd-token').value;
+ const connectionid = document.getElementById('fd-connectionid').value;
+ const key = document.getElementById('fd-key').value;
+ const params = new URLSearchParams({ connectionid, key });
+ log('HTTP /get/file', `→ ${params.toString()}`, 'log-info');
+ try {
+ const resp = await fetch(baseUrl() + '/get/file', { method: 'POST', headers: { token }, body: params });
+ const text = await resp.text();
+ log(`HTTP ${resp.status}`, text, resp.ok ? 'log-http' : 'log-err');
+ } catch(e) {
+ log('HTTP ERR', e.message, 'log-err');
+ }
+ }
+
function setWsStatus(connected) {
const el = document.getElementById('ws-status');
if (!el) return;
diff --git a/main.go b/main.go
index 302580e..0042875 100644
--- a/main.go
+++ b/main.go
@@ -5,7 +5,8 @@ import (
"log"
"net/http"
- http2 "go-socket/packages/http"
+ "go-socket/packages/httpRequest"
+ "go-socket/packages/minio"
"go-socket/packages/postgresql"
"go-socket/packages/wsServer"
)
@@ -13,6 +14,11 @@ import (
func withCORS(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Headers", "token, Content-Type")
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
h(w, r)
}
}
@@ -20,21 +26,24 @@ func withCORS(h http.HandlerFunc) http.HandlerFunc {
func main() {
ctx := context.Background()
postgresql.Init(ctx)
+ minio.Init(ctx)
- http.HandleFunc("/new/user", withCORS(http2.HandleUserNew))
- http.HandleFunc("/new/connection", withCORS(http2.HandleUserNewConnection))
- http.HandleFunc("/new/token", withCORS(http2.HandleUserNewToken))
- http.HandleFunc("/mod/user/appearence", withCORS(http2.HandleUserModifyAppearance))
- http.HandleFunc("/mod/user/about", withCORS(http2.HandleUserModifyAbout))
- http.HandleFunc("/mod/connection/accept", withCORS(http2.HandleUserElevateConnection))
+ http.HandleFunc("/new/user", withCORS(httpRequest.HandleUserNew))
+ http.HandleFunc("/new/connection", withCORS(httpRequest.HandleUserNewConnection))
+ http.HandleFunc("/new/token", withCORS(httpRequest.HandleUserNewToken))
+ http.HandleFunc("/new/file", withCORS(httpRequest.HandleFileUpload))
+ http.HandleFunc("/mod/user/appearence", withCORS(httpRequest.HandleUserModifyAppearance))
+ http.HandleFunc("/mod/user/about", withCORS(httpRequest.HandleUserModifyAbout))
+ http.HandleFunc("/mod/connection/accept", withCORS(httpRequest.HandleUserElevateConnection))
- http.HandleFunc("/get/connections", withCORS(http2.HandleUserGetConnections))
- http.HandleFunc("/get/connection/messages", withCORS(http2.HandleUserGetConnectionMessages))
+ http.HandleFunc("/get/connections", withCORS(httpRequest.HandleUserGetConnections))
+ http.HandleFunc("/get/connection/messages", withCORS(httpRequest.HandleUserGetConnectionMessages))
+ http.HandleFunc("/get/file", withCORS(httpRequest.HandleFileDownload))
- http.HandleFunc("/del/user", withCORS(http2.HandleUserDelete))
- http.HandleFunc("/del/connection", withCORS(http2.HandleUserDeleteConnection))
+ http.HandleFunc("/del/user", withCORS(httpRequest.HandleUserDelete))
+ http.HandleFunc("/del/connection", withCORS(httpRequest.HandleUserDeleteConnection))
- http.HandleFunc("/msg/user", withCORS(http2.HandleDm))
+ http.HandleFunc("/msg/user", withCORS(httpRequest.HandleDm))
http.HandleFunc("/ws", wsServer.ServeWsConnection)
log.Println("listening on :8080")
diff --git a/packages/globals/globals.go b/packages/globals/globals.go
index 7f8b626..5a9d428 100644
--- a/packages/globals/globals.go
+++ b/packages/globals/globals.go
@@ -7,7 +7,7 @@ const (
FileStorageBucketName string = "communicator"
MaxPostBytes uint32 = 4 << 10
MaxPostWithFileBytes uint32 = 1 << 30
- FileProcessingPartSize uint64 = 24 << 10
+ FileProcessingPartSize uint64 = 12 << 20
FileProcessingThreads uint = 3
FileDownloadLinkTtl time.Duration = 24 * time.Hour
)
diff --git a/packages/http/connectionsAndDms.go b/packages/httpRequest/connectionsAndDms.go
similarity index 95%
rename from packages/http/connectionsAndDms.go
rename to packages/httpRequest/connectionsAndDms.go
index 6ec4898..b53f81d 100644
--- a/packages/http/connectionsAndDms.go
+++ b/packages/httpRequest/connectionsAndDms.go
@@ -1,10 +1,11 @@
-package http
+package httpRequest
import (
json2 "encoding/json"
"maps"
"net/http"
"slices"
+ "strings"
"time"
"go-socket/packages/Enums/ConnectionState"
@@ -43,11 +44,18 @@ func HandleDm(response http.ResponseWriter, request *http.Request) {
}
msgContent := request.FormValue("msgContent")
- if msgContent == "" {
+ attachedFile := request.FormValue("attachedFile")
+
+ if msgContent == "" && attachedFile == "" {
http.Error(response, "empty msgContent", http.StatusBadRequest)
return
}
+ if attachedFile != "" && !strings.HasPrefix(attachedFile, targetConnection.String()+"/") {
+ http.Error(response, "invalid attachedFile", http.StatusBadRequest)
+ return
+ }
+
var target *types.User
if user.Id == conn.RequestorId {
@@ -64,11 +72,12 @@ func HandleDm(response http.ResponseWriter, request *http.Request) {
return
}
message := &types.Message{
- Id: uuid.New(),
- Content: msgContent,
- CreatedAt: time.Now(),
- Sender: user.Id,
- Receiver: conn.Id,
+ Id: uuid.New(),
+ Content: msgContent,
+ AttachedFile: attachedFile,
+ CreatedAt: time.Now(),
+ Sender: user.Id,
+ Receiver: conn.Id,
}
wsServer.WsSendMessageCloseIfTimeout(target, types.WsEventMessage{
diff --git a/packages/http/file.go b/packages/httpRequest/file.go
similarity index 50%
rename from packages/http/file.go
rename to packages/httpRequest/file.go
index 6add618..48bd979 100644
--- a/packages/http/file.go
+++ b/packages/httpRequest/file.go
@@ -1,12 +1,12 @@
-package http
+package httpRequest
import (
+ "net/http"
+ "strings"
+
"go-socket/packages/convertions"
"go-socket/packages/globals"
"go-socket/packages/minio"
- "net/http"
-
- "github.com/google/uuid"
)
func HandleFileUpload(response http.ResponseWriter, request *http.Request) {
@@ -47,17 +47,54 @@ func HandleFileUpload(response http.ResponseWriter, request *http.Request) {
defer file.Close()
contentType := header.Header.Get("Content-Type")
+ key := minio.GetKey(connectionId, contentType)
- key :=
-
- if err = minio.Upload(ctx, fileId.String(), file, header.Size, contentType, map[string]string{
- "orginalName": header.Filename,
- "uploaderId": user.Id.String(),
+ if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{
+ "originalName": header.Filename,
+ "uploaderId": user.Id.String(),
}); err != nil {
http.Error(response, "upload failed", http.StatusInternalServerError)
return
}
- response.WriteHeader(http.StatusAccepted)
- response.Write([]byte(fileId.String()))
+ response.WriteHeader(http.StatusCreated)
+ response.Write([]byte(key))
+}
+
+func HandleFileDownload(response http.ResponseWriter, request *http.Request) {
+ if !postValidCheckWithResponseOnFail(&response, request, false) {
+ return
+ }
+ ctx := request.Context()
+
+ user, err := getUserByToken(ctx, request.Header.Get("token"))
+ if err != nil {
+ http.Error(response, "invalid token", http.StatusUnauthorized)
+ return
+ }
+
+ connectionId, err := convertions.ConvertStringUuid(request.FormValue("connectionid"))
+ if err != nil {
+ http.Error(response, "invalid connectionid", http.StatusBadRequest)
+ return
+ }
+ if _, ok := user.Connections[connectionId]; !ok {
+ http.Error(response, "no such connection", http.StatusUnauthorized)
+ return
+ }
+
+ key := request.FormValue("key")
+ if !strings.HasPrefix(key, connectionId.String()+"/") {
+ http.Error(response, "no such file", http.StatusUnauthorized)
+ return
+ }
+
+ url, err := minio.GetDownloadUrl(ctx, key)
+ if err != nil {
+ http.Error(response, "no such file", http.StatusUnauthorized)
+ return
+ }
+
+ response.WriteHeader(http.StatusOK)
+ response.Write([]byte(url.String()))
}
diff --git a/packages/http/get.go b/packages/httpRequest/get.go
similarity index 97%
rename from packages/http/get.go
rename to packages/httpRequest/get.go
index c9afe68..8dbf81c 100644
--- a/packages/http/get.go
+++ b/packages/httpRequest/get.go
@@ -1,4 +1,4 @@
-package http
+package httpRequest
import (
"context"
diff --git a/packages/http/helper.go b/packages/httpRequest/helper.go
similarity index 96%
rename from packages/http/helper.go
rename to packages/httpRequest/helper.go
index 1eea783..d1fe383 100644
--- a/packages/http/helper.go
+++ b/packages/httpRequest/helper.go
@@ -1,4 +1,4 @@
-package http
+package httpRequest
import (
"net/http"
diff --git a/packages/http/user.go b/packages/httpRequest/user.go
similarity index 97%
rename from packages/http/user.go
rename to packages/httpRequest/user.go
index 1bed768..0f5e8d9 100644
--- a/packages/http/user.go
+++ b/packages/httpRequest/user.go
@@ -1,4 +1,4 @@
-package http
+package httpRequest
import (
json2 "encoding/json"
@@ -26,7 +26,7 @@ func HandleUserNewToken(response http.ResponseWriter, request *http.Request) {
return
}
- password := request.FormValue("passwords")
+ password := request.FormValue("password")
if len(password) < 8 {
http.Error(response, "no or short passwords", http.StatusBadRequest)
@@ -85,7 +85,7 @@ func HandleUserNew(response http.ResponseWriter, request *http.Request) {
return
}
- password := request.FormValue("passwords")
+ password := request.FormValue("password")
if len(password) < 8 {
http.Error(response, "no or short passwords", http.StatusBadRequest)
return
diff --git a/packages/minio/minio.go b/packages/minio/minio.go
index e496cf3..1b454a1 100644
--- a/packages/minio/minio.go
+++ b/packages/minio/minio.go
@@ -5,42 +5,27 @@ import (
"io"
"mime"
"net/url"
+ "strconv"
+ "time"
"go-socket/packages/globals"
+ "github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var minClient *minio.Client
-var canonicalExt = map[string]string{
- "image/jpeg": ".jpg",
- "image/png": ".png",
- "image/gif": ".gif",
- "image/webp": ".webp",
- "video/mp4": ".mp4",
- "application/pdf": ".pdf",
-}
-
-func extensionFromContentType(ct string) string {
- if ext, ok := canonicalExt[ct]; ok {
- return ext
+func GetKey(connectionId uuid.UUID, mimeType string) string {
+ extensions, err := mime.ExtensionsByType(mimeType)
+ if err != nil || len(extensions) == 0 {
+ extensions = []string{".unknown"}
}
- exts, err := mime.ExtensionsByType(ct)
- if err != nil || len(exts) == 0 {
- return ".unk"
- }
- return exts[0]
+ return connectionId.String() + "/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + extensions[0]
}
-func GetKey(hash string, contentType string) string {
- return hash + extensionFromContentType(contentType)
-}
-
-func Init() {
- ctx := context.Background()
-
+func Init(ctx context.Context) {
var err error
minClient, err = minio.New("localhost:9000", &minio.Options{
Creds: credentials.NewStaticV4("root", "change_to_env", ""),
@@ -58,7 +43,10 @@ func Init() {
if !exists {
err = minClient.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{})
if err != nil {
- return
+ exists, checkErr := minClient.BucketExists(ctx, globals.FileStorageBucketName)
+ if checkErr != nil || !exists {
+ panic(err)
+ }
}
}
diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go
index 230d1e1..3aa3d5e 100644
--- a/packages/postgresql/postgresql.go
+++ b/packages/postgresql/postgresql.go
@@ -60,7 +60,8 @@ func Init(ctx context.Context) {
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
receiver_id UUID NOT NULL REFERENCES user_connections(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
- content TEXT NOT NULL
+ content TEXT NOT NULL,
+ attached_file TEXT NOT NULL DEFAULT ''
)
`)
if err != nil {
@@ -182,21 +183,21 @@ func ConnectionUpdateState(ctx context.Context, conn *types.Connection) error {
func ConnectionMessageSave(ctx context.Context, message *types.Message) error {
if message.Id != (uuid.UUID{}) {
_, err := dbConn.Exec(ctx, `
- INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4, $5)
- `, message.Id, message.Sender, message.Receiver, message.CreatedAt, message.Content)
+ INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content, attached_file) VALUES ($1, $2, $3, $4, $5, $6)
+ `, message.Id, message.Sender, message.Receiver, message.CreatedAt, message.Content, message.AttachedFile)
return err
}
return dbConn.QueryRow(ctx, `
- INSERT INTO direct_messages (sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4)
+ INSERT INTO direct_messages (sender_id, receiver_id, created_at, content, attached_file) VALUES ($1, $2, $3, $4, $5)
RETURNING id
- `, message.Sender, message.Receiver, message.CreatedAt, message.Content).Scan(&message.Id)
+ `, message.Sender, message.Receiver, message.CreatedAt, message.Content, message.AttachedFile).Scan(&message.Id)
}
func ConnectionGetMessagesBefore(ctx context.Context, before time.Time, connection uuid.UUID, cap uint32) ([]*types.Message, error) {
rows, err := dbConn.Query(ctx, `
- SELECT id, sender_id, receiver_id, created_at, content
+ SELECT id, sender_id, receiver_id, created_at, content, attached_file
FROM (
- SELECT id, sender_id, receiver_id, created_at, content
+ SELECT id, sender_id, receiver_id, created_at, content, attached_file
FROM direct_messages
WHERE receiver_id = $1
AND created_at < $2
@@ -213,7 +214,7 @@ func ConnectionGetMessagesBefore(ctx context.Context, before time.Time, connecti
messages := make([]*types.Message, 0, cap)
for rows.Next() {
msg := &types.Message{}
- if err = rows.Scan(&msg.Id, &msg.Sender, &msg.Receiver, &msg.CreatedAt, &msg.Content); err != nil {
+ if err = rows.Scan(&msg.Id, &msg.Sender, &msg.Receiver, &msg.CreatedAt, &msg.Content, &msg.AttachedFile); err != nil {
return nil, err
}
messages = append(messages, msg)
diff --git a/packages/types/types.go b/packages/types/types.go
index d59ab40..04177a2 100644
--- a/packages/types/types.go
+++ b/packages/types/types.go
@@ -82,12 +82,12 @@ type ConnectionElevationData struct {
}
type Message struct {
- Id uuid.UUID `json:"id"`
- AttachedMedia string `json:"attachedMedia"`
- Content string `json:"content"`
- CreatedAt time.Time `json:"createdAt"`
- Sender uuid.UUID `json:"sender"`
- Receiver uuid.UUID `json:"receiver"`
+ Id uuid.UUID `json:"id"`
+ AttachedFile string `json:"attachedFile"`
+ Content string `json:"content"`
+ CreatedAt time.Time `json:"createdAt"`
+ Sender uuid.UUID `json:"sender"`
+ Receiver uuid.UUID `json:"receiver"`
}
type LoginReturn struct {
@@ -96,7 +96,7 @@ type LoginReturn struct {
}
type WsEventMessage struct {
- Type WsEventType.WsEventType `json:"types"`
+ Type WsEventType.WsEventType `json:"type"`
Event any `json:"event"`
}