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:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+45
-11
@@ -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 {
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user