diff --git a/packages/globals/globals.go b/packages/globals/globals.go index 8126ea8..7501453 100644 --- a/packages/globals/globals.go +++ b/packages/globals/globals.go @@ -1,6 +1,9 @@ package globals +import "time" + const ( - MaxDirectMsgCache uint32 = 12 - FileStorageBucketName string = "communicator" + MaxDirectMsgCache uint32 = 12 + FileStorageBucketName string = "communicator" + FileDownloadLinkTtl time.Duration = 24 * time.Hour ) diff --git a/packages/minio/minio.go b/packages/minio/minio.go index a2c2107..3844620 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -2,20 +2,47 @@ package minio import ( "context" - "go-socket/packages/globals" "io" + "mime" + "net/url" + + "go-socket/packages/globals" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) -var dbConn *minio.Client +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 + } + exts, err := mime.ExtensionsByType(ct) + if err != nil || len(exts) == 0 { + return ".unk" + } + return exts[0] +} + +func getKey(hash string, contentType string) string { + return hash + extensionFromContentType(contentType) +} func MinInit() { ctx := context.Background() var err error - dbConn, err = minio.New("localhost:9000", &minio.Options{ + minClient, err = minio.New("localhost:9000", &minio.Options{ Creds: credentials.NewStaticV4("root", "change_to_env", ""), Secure: false, }) // TODO change in production @@ -23,22 +50,29 @@ func MinInit() { panic(err) } - exists, err := dbConn.BucketExists(ctx, "main") + exists, err := minClient.BucketExists(ctx, globals.FileStorageBucketName) if err != nil { panic(err) } if !exists { - err = dbConn.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{}) + err = minClient.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{}) if err != nil { return } } -} - -func putFile(ctx context.Context, key string, reader io.Reader, size uint32, contentType string) error { - dbConn.PutObject(ctx, globals.FileStorageBucketName, key, reader, int64(size), minio.PutObjectOptions{ - ContentType: contentType, - }) } + +func upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { + _, err := minClient.PutObject(ctx, globals.FileStorageBucketName, key, body, size, + minio.PutObjectOptions{ + ContentType: contentType, + }) + return err +} + +func getDownloadUrl(ctx context.Context, key string) (*url.URL, error) { + u, err := minClient.PresignedGetObject(ctx, globals.FileStorageBucketName, key, globals.FileDownloadLinkTtl, nil) + return u, err +} diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index 4473bd0..771ab47 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -67,6 +67,28 @@ func PgInit(ctx context.Context) { panic(err) } + _, err = dbConn.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS files_metadata ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + uploader_id UUID NOT NULL REFERENCES users(id), + name TEXT, + hash TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `) + if err != nil { + panic(err) + } + + _, err = dbConn.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS files_metadata_connections ( + file_metadata_id UUID NOT NULL REFERENCES files_metadata(id) ON DELETE CASCADE, + connection_id UUID NOT NULL REFERENCES user_connections(id) ON DELETE CASCADE + ) + `) + if err != nil { + panic(err) + } } func PgUserSave(ctx context.Context, user *types.User) error { @@ -221,3 +243,57 @@ func PgConnectionGetMessagesBefore(ctx context.Context, before time.Time, connec } return messages, rows.Err() } + +func PgFileMetadataSave(ctx context.Context, metadata *types.FileMetadata) error { + _, err := dbConn.Exec(ctx, ` + INSERT INTO files_metadata (id, uploader_id, name, hash, created_at) VALUES ($1, $2, $3, $4, $5) + `, metadata.Id, metadata.UploaderId, metadata.Name, metadata.Hash, metadata.CreatedAt) + return err +} + +func PgFileMetadataGet(ctx context.Context, metadata *types.FileMetadata) error { + return dbConn.QueryRow(ctx, ` + SELECT uploader_id, name, hash, created_at FROM files_metadata WHERE id = $1 + `, metadata.Id).Scan(&metadata.UploaderId, &metadata.Name, &metadata.Hash, &metadata.CreatedAt) +} + +func PgFileMetadataDelete(ctx context.Context, metadataId *uuid.UUID) error { + _, err := dbConn.Exec(ctx, ` + DELETE FROM files_metadata WHERE id = $1 + `, metadataId) + return err +} + +func PgFileConnectionsAdd(ctx context.Context, metadataId *uuid.UUID, connectionId *uuid.UUID) error { + _, err := dbConn.Exec(ctx, ` + INSERT INTO files_metadata_connections (file_metadata_id, connection_id) VALUES ($1, $2) + `, metadataId, connectionId) + return err +} + +func PgFileConnectionsGet(ctx context.Context, metadata *types.FileMetadata) error { + rows, err := dbConn.Query(ctx, ` + SELECT connection_id FROM files_metadata_connections WHERE file_metadata_id = $1 + `, metadata.Id) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + connectionId := uuid.UUID{} + if err = rows.Scan(&connectionId); err != nil { + return err + } + metadata.Connections = append(metadata.Connections, connectionId) + } + return rows.Err() +} + +func PgFileMetadataHashExists(ctx context.Context, hash *types.Sha256Hash) (bool, error) { + var exists bool + err := dbConn.QueryRow(ctx, ` + SELECT EXISTS (SELECT FROM files_metadata WHERE hash = $1) + `, hash).Scan(&exists) + return exists, err +} diff --git a/packages/types/types.go b/packages/types/types.go index 90a0cb0..8d4c11f 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -1,6 +1,7 @@ package types import ( + "crypto/sha256" "math/rand/v2" "sync" "time" @@ -14,6 +15,7 @@ import ( ) type Rgba [4]uint8 +type Sha256Hash [sha256.Size]byte func (r Rgba) GetRandom() *Rgba { for i := range r { @@ -103,8 +105,11 @@ type WsAuthMessage struct { Error string `json:"error"` } -type Media struct { - Hash string - Owner uint32 - CreatedAt time.Time +type FileMetadata struct { + CreatedAt time.Time `json:"createdAt"` + Connections []uuid.UUID `json:"connections"` + Name string `json:"name"` + Hash Sha256Hash `json:"hash"` + Id uuid.UUID `json:"id"` + UploaderId uuid.UUID `json:"uploaderId"` }