add file storage: metadata schema, minio upload/download, content-type extension mapping

- add files_metadata and files_metadata_connections tables with CRUD helpers
- add FileMetadata type and Sha256Hash typedef; replace Media struct
- add minio upload, presigned download URL, and key generation
- fix bucket existence check to use FileStorageBucketName instead of hardcoded "main"
- fix files_metadata_connections table name and trailing comma in DDL
- fix column name original -> name in files_metadata schema
- add canonical MIME-to-extension map with .unk fallback
- add FileDownloadLinkTtl constant (24h)
This commit is contained in:
2026-04-14 18:55:22 +02:00
parent 39a28ee888
commit 48d3c6f857
4 changed files with 135 additions and 17 deletions
+3
View File
@@ -1,6 +1,9 @@
package globals package globals
import "time"
const ( const (
MaxDirectMsgCache uint32 = 12 MaxDirectMsgCache uint32 = 12
FileStorageBucketName string = "communicator" FileStorageBucketName string = "communicator"
FileDownloadLinkTtl time.Duration = 24 * time.Hour
) )
+42 -8
View File
@@ -2,20 +2,47 @@ package minio
import ( import (
"context" "context"
"go-socket/packages/globals"
"io" "io"
"mime"
"net/url"
"go-socket/packages/globals"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "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() { func MinInit() {
ctx := context.Background() ctx := context.Background()
var err error 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", ""), Creds: credentials.NewStaticV4("root", "change_to_env", ""),
Secure: false, Secure: false,
}) // TODO change in production }) // TODO change in production
@@ -23,22 +50,29 @@ func MinInit() {
panic(err) panic(err)
} }
exists, err := dbConn.BucketExists(ctx, "main") exists, err := minClient.BucketExists(ctx, globals.FileStorageBucketName)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if !exists { if !exists {
err = dbConn.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{}) err = minClient.MakeBucket(ctx, globals.FileStorageBucketName, minio.MakeBucketOptions{})
if err != nil { if err != nil {
return return
} }
} }
} }
func putFile(ctx context.Context, key string, reader io.Reader, size uint32, contentType string) error { func upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error {
dbConn.PutObject(ctx, globals.FileStorageBucketName, key, reader, int64(size), minio.PutObjectOptions{ _, err := minClient.PutObject(ctx, globals.FileStorageBucketName, key, body, size,
minio.PutObjectOptions{
ContentType: contentType, 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
} }
+76
View File
@@ -67,6 +67,28 @@ func PgInit(ctx context.Context) {
panic(err) 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 { 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() 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
}
+9 -4
View File
@@ -1,6 +1,7 @@
package types package types
import ( import (
"crypto/sha256"
"math/rand/v2" "math/rand/v2"
"sync" "sync"
"time" "time"
@@ -14,6 +15,7 @@ import (
) )
type Rgba [4]uint8 type Rgba [4]uint8
type Sha256Hash [sha256.Size]byte
func (r Rgba) GetRandom() *Rgba { func (r Rgba) GetRandom() *Rgba {
for i := range r { for i := range r {
@@ -103,8 +105,11 @@ type WsAuthMessage struct {
Error string `json:"error"` Error string `json:"error"`
} }
type Media struct { type FileMetadata struct {
Hash string CreatedAt time.Time `json:"createdAt"`
Owner uint32 Connections []uuid.UUID `json:"connections"`
CreatedAt time.Time Name string `json:"name"`
Hash Sha256Hash `json:"hash"`
Id uuid.UUID `json:"id"`
UploaderId uuid.UUID `json:"uploaderId"`
} }