Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
509
internal/store/postgres/media.go
Normal file
509
internal/store/postgres/media.go
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// MediaStore 用 PostgreSQL 实现 store.MediaStore(媒体元数据 + blob 索引)。
|
||||
type MediaStore struct {
|
||||
db sqlcgen.DBTX
|
||||
q *sqlcgen.Queries
|
||||
}
|
||||
|
||||
// NewMediaStore 基于 pgx 连接池(或事务)创建 MediaStore。
|
||||
func NewMediaStore(db sqlcgen.DBTX) *MediaStore {
|
||||
return &MediaStore{db: db, q: sqlcgen.New(db)}
|
||||
}
|
||||
|
||||
// bytesOrEmpty 把 nil []byte 归一为空切片,避免落入 NOT NULL bytea 列时被当作 NULL。
|
||||
func bytesOrEmpty(b []byte) []byte {
|
||||
if b == nil {
|
||||
return []byte{}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var _ store.MediaStore = (*MediaStore)(nil)
|
||||
|
||||
// ---- 上传分片 ----
|
||||
|
||||
func (s *MediaStore) SaveFilePart(ctx context.Context, part domain.UploadPart) error {
|
||||
return s.q.SaveUploadPart(ctx, sqlcgen.SaveUploadPartParams{
|
||||
OwnerUserID: part.OwnerUserID,
|
||||
FileID: part.FileID,
|
||||
Part: int32(part.Part),
|
||||
TotalParts: int32(part.TotalParts),
|
||||
IsBig: part.Big,
|
||||
Bytes: part.Bytes,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) LoadFileParts(ctx context.Context, ownerUserID, fileID int64) ([]domain.UploadPart, error) {
|
||||
rows, err := s.q.ListUploadParts(ctx, sqlcgen.ListUploadPartsParams{OwnerUserID: ownerUserID, FileID: fileID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]domain.UploadPart, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, domain.UploadPart{
|
||||
OwnerUserID: ownerUserID,
|
||||
FileID: fileID,
|
||||
Part: int(r.Part),
|
||||
TotalParts: int(r.TotalParts),
|
||||
Big: r.IsBig,
|
||||
Bytes: r.Bytes,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) DeleteFileParts(ctx context.Context, ownerUserID, fileID int64) error {
|
||||
return s.q.DeleteUploadParts(ctx, sqlcgen.DeleteUploadPartsParams{OwnerUserID: ownerUserID, FileID: fileID})
|
||||
}
|
||||
|
||||
// ---- blob 索引 ----
|
||||
|
||||
func (s *MediaStore) PutFileBlob(ctx context.Context, blob domain.FileBlob) error {
|
||||
backend := string(blob.Backend)
|
||||
if backend == "" {
|
||||
backend = string(domain.MediaBackendLocalFS)
|
||||
}
|
||||
sha := blob.SHA256
|
||||
if sha == nil {
|
||||
sha = []byte{} // 列为 NOT NULL;nil []byte 会被 pgx 当作 NULL。
|
||||
}
|
||||
return s.q.PutFileBlob(ctx, sqlcgen.PutFileBlobParams{
|
||||
LocationKey: blob.LocationKey,
|
||||
Backend: backend,
|
||||
ObjectKey: blob.ObjectKey,
|
||||
Size: blob.Size,
|
||||
Sha256: sha,
|
||||
MimeType: blob.MimeType,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetFileBlob(ctx context.Context, locationKey string) (domain.FileBlob, bool, error) {
|
||||
row, err := s.q.GetFileBlob(ctx, locationKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.FileBlob{}, false, nil
|
||||
}
|
||||
return domain.FileBlob{}, false, err
|
||||
}
|
||||
return domain.FileBlob{
|
||||
LocationKey: row.LocationKey,
|
||||
Backend: domain.MediaBackend(row.Backend),
|
||||
ObjectKey: row.ObjectKey,
|
||||
Size: row.Size,
|
||||
SHA256: row.Sha256,
|
||||
MimeType: row.MimeType,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// ---- 文档 ----
|
||||
|
||||
func (s *MediaStore) PutDocument(ctx context.Context, doc domain.Document) error {
|
||||
attrs, err := jsonArrayOrEmpty(doc.Attributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
thumbs, err := jsonArrayOrEmpty(doc.Thumbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.q.PutDocument(ctx, sqlcgen.PutDocumentParams{
|
||||
ID: doc.ID,
|
||||
AccessHash: doc.AccessHash,
|
||||
FileReference: bytesOrEmpty(doc.FileReference),
|
||||
Date: int32(doc.Date),
|
||||
MimeType: doc.MimeType,
|
||||
Size: doc.Size,
|
||||
DcID: int32(doc.DCID),
|
||||
AttributesJson: attrs,
|
||||
ThumbsJson: thumbs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetDocument(ctx context.Context, id int64) (domain.Document, bool, error) {
|
||||
row, err := s.q.GetDocument(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Document{}, false, nil
|
||||
}
|
||||
return domain.Document{}, false, err
|
||||
}
|
||||
doc, err := documentFromRow(row)
|
||||
if err != nil {
|
||||
return domain.Document{}, false, err
|
||||
}
|
||||
return doc, true, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetDocuments(ctx context.Context, ids []int64) ([]domain.Document, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.q.GetDocuments(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]domain.Document, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
doc, err := documentFromRow(sqlcgen.GetDocumentRow(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, doc)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func documentFromRow(row sqlcgen.GetDocumentRow) (domain.Document, error) {
|
||||
attrs, err := decodeDocumentAttributes(row.AttributesJson)
|
||||
if err != nil {
|
||||
return domain.Document{}, err
|
||||
}
|
||||
thumbs, err := decodePhotoSizes(row.ThumbsJson)
|
||||
if err != nil {
|
||||
return domain.Document{}, err
|
||||
}
|
||||
return domain.Document{
|
||||
ID: row.ID,
|
||||
AccessHash: row.AccessHash,
|
||||
FileReference: row.FileReference,
|
||||
Date: int(row.Date),
|
||||
MimeType: row.MimeType,
|
||||
Size: row.Size,
|
||||
DCID: int(row.DcID),
|
||||
Attributes: attrs,
|
||||
Thumbs: thumbs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ---- 照片 ----
|
||||
|
||||
func (s *MediaStore) PutPhoto(ctx context.Context, photo domain.Photo) error {
|
||||
sizes, err := jsonArrayOrEmpty(photo.Sizes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.q.PutPhoto(ctx, sqlcgen.PutPhotoParams{
|
||||
ID: photo.ID,
|
||||
AccessHash: photo.AccessHash,
|
||||
FileReference: bytesOrEmpty(photo.FileReference),
|
||||
Date: int32(photo.Date),
|
||||
DcID: int32(photo.DCID),
|
||||
HasStickers: photo.HasStickers,
|
||||
SizesJson: sizes,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetPhoto(ctx context.Context, id int64) (domain.Photo, bool, error) {
|
||||
row, err := s.q.GetPhoto(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Photo{}, false, nil
|
||||
}
|
||||
return domain.Photo{}, false, err
|
||||
}
|
||||
sizes, err := decodePhotoSizes(row.SizesJson)
|
||||
if err != nil {
|
||||
return domain.Photo{}, false, err
|
||||
}
|
||||
return domain.Photo{
|
||||
ID: row.ID,
|
||||
AccessHash: row.AccessHash,
|
||||
FileReference: row.FileReference,
|
||||
Date: int(row.Date),
|
||||
DCID: int(row.DcID),
|
||||
HasStickers: row.HasStickers,
|
||||
Sizes: sizes,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// ---- 贴纸集 ----
|
||||
|
||||
func (s *MediaStore) PutStickerSet(ctx context.Context, set domain.StickerSet) error {
|
||||
thumbs, err := jsonArrayOrEmpty(set.Thumbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
docIDs, err := jsonArrayOrEmpty(set.DocumentIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
packs, err := jsonArrayOrEmpty(set.Packs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kind := string(set.Kind)
|
||||
if kind == "" {
|
||||
kind = string(domain.StickerSetKindStickers)
|
||||
}
|
||||
return s.q.PutStickerSet(ctx, sqlcgen.PutStickerSetParams{
|
||||
ID: set.ID,
|
||||
AccessHash: set.AccessHash,
|
||||
ShortName: set.ShortName,
|
||||
Title: set.Title,
|
||||
Count: int32(set.Count),
|
||||
Hash: int32(set.Hash),
|
||||
SetKind: kind,
|
||||
Official: set.Official,
|
||||
Animated: set.Animated,
|
||||
Videos: set.Videos,
|
||||
Emojis: set.Emojis,
|
||||
Masks: set.Masks,
|
||||
Installed: set.Installed,
|
||||
Archived: set.Archived,
|
||||
InstalledDate: int32(set.InstalledDate),
|
||||
ThumbDocumentID: set.ThumbDocumentID,
|
||||
ThumbsJson: thumbs,
|
||||
ThumbDcID: int32(set.ThumbDCID),
|
||||
ThumbVersion: int32(set.ThumbVersion),
|
||||
DocumentIdsJson: docIDs,
|
||||
PacksJson: packs,
|
||||
SortOrder: int32(set.SortOrder),
|
||||
SystemKey: set.SystemKey,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetStickerSetByID(ctx context.Context, id int64) (domain.StickerSet, bool, error) {
|
||||
row, err := s.q.GetStickerSetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.StickerSet{}, false, nil
|
||||
}
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
return stickerSetFromRow(row)
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetStickerSetByShortName(ctx context.Context, shortName string) (domain.StickerSet, bool, error) {
|
||||
row, err := s.q.GetStickerSetByShortName(ctx, shortName)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.StickerSet{}, false, nil
|
||||
}
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
|
||||
}
|
||||
|
||||
func (s *MediaStore) GetStickerSetBySystemKey(ctx context.Context, systemKey string) (domain.StickerSet, bool, error) {
|
||||
row, err := s.q.GetStickerSetBySystemKey(ctx, systemKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.StickerSet{}, false, nil
|
||||
}
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
return stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(row))
|
||||
}
|
||||
|
||||
func (s *MediaStore) ListStickerSets(ctx context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error) {
|
||||
rows, err := s.q.ListStickerSetsByKind(ctx, string(kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]domain.StickerSet, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
set, _, err := stickerSetFromRow(sqlcgen.GetStickerSetByIDRow(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, set)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) CountStickerSets(ctx context.Context) (int, error) {
|
||||
n, err := s.q.CountStickerSets(ctx)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
func stickerSetFromRow(row sqlcgen.GetStickerSetByIDRow) (domain.StickerSet, bool, error) {
|
||||
thumbs, err := decodePhotoSizes(row.ThumbsJson)
|
||||
if err != nil {
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
docIDs, err := decodeInt64Slice(row.DocumentIdsJson)
|
||||
if err != nil {
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
packs, err := decodeStickerPacks(row.PacksJson)
|
||||
if err != nil {
|
||||
return domain.StickerSet{}, false, err
|
||||
}
|
||||
return domain.StickerSet{
|
||||
ID: row.ID,
|
||||
AccessHash: row.AccessHash,
|
||||
ShortName: row.ShortName,
|
||||
Title: row.Title,
|
||||
Count: int(row.Count),
|
||||
Hash: int(row.Hash),
|
||||
Kind: domain.StickerSetKind(row.SetKind),
|
||||
Official: row.Official,
|
||||
Animated: row.Animated,
|
||||
Videos: row.Videos,
|
||||
Emojis: row.Emojis,
|
||||
Masks: row.Masks,
|
||||
Installed: row.Installed,
|
||||
Archived: row.Archived,
|
||||
InstalledDate: int(row.InstalledDate),
|
||||
ThumbDocumentID: row.ThumbDocumentID,
|
||||
Thumbs: thumbs,
|
||||
ThumbDCID: int(row.ThumbDcID),
|
||||
ThumbVersion: int(row.ThumbVersion),
|
||||
DocumentIDs: docIDs,
|
||||
Packs: packs,
|
||||
SortOrder: int(row.SortOrder),
|
||||
SystemKey: row.SystemKey,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// ---- 可用 reaction ----
|
||||
|
||||
func (s *MediaStore) PutAvailableReaction(ctx context.Context, r domain.AvailableReaction) error {
|
||||
return s.q.PutAvailableReaction(ctx, sqlcgen.PutAvailableReactionParams{
|
||||
Reaction: r.Reaction,
|
||||
Title: r.Title,
|
||||
Inactive: r.Inactive,
|
||||
Premium: r.Premium,
|
||||
StaticIconID: r.StaticIconID,
|
||||
AppearAnimationID: r.AppearAnimationID,
|
||||
SelectAnimationID: r.SelectAnimationID,
|
||||
ActivateAnimationID: r.ActivateAnimationID,
|
||||
EffectAnimationID: r.EffectAnimationID,
|
||||
AroundAnimationID: r.AroundAnimationID,
|
||||
CenterIconID: r.CenterIconID,
|
||||
SortOrder: int32(r.Order),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) ListAvailableReactions(ctx context.Context) ([]domain.AvailableReaction, error) {
|
||||
rows, err := s.q.ListAvailableReactions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]domain.AvailableReaction, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, domain.AvailableReaction{
|
||||
Reaction: r.Reaction,
|
||||
Title: r.Title,
|
||||
Inactive: r.Inactive,
|
||||
Premium: r.Premium,
|
||||
StaticIconID: r.StaticIconID,
|
||||
AppearAnimationID: r.AppearAnimationID,
|
||||
SelectAnimationID: r.SelectAnimationID,
|
||||
ActivateAnimationID: r.ActivateAnimationID,
|
||||
EffectAnimationID: r.EffectAnimationID,
|
||||
AroundAnimationID: r.AroundAnimationID,
|
||||
CenterIconID: r.CenterIconID,
|
||||
Order: int(r.SortOrder),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) CountAvailableReactions(ctx context.Context) (int, error) {
|
||||
n, err := s.q.CountAvailableReactions(ctx)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// ---- 头像历史 ----
|
||||
|
||||
func (s *MediaStore) AddProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID, photoID int64, date int) error {
|
||||
next, err := s.q.NextProfilePhotoOrder(ctx, sqlcgen.NextProfilePhotoOrderParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.q.AddProfilePhoto(ctx, sqlcgen.AddProfilePhotoParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
PhotoID: photoID,
|
||||
Date: int32(date),
|
||||
SortOrder: next + 1,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID int64) (int64, bool, error) {
|
||||
id, err := s.q.CurrentProfilePhoto(ctx, sqlcgen.CurrentProfilePhotoParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, false, nil
|
||||
}
|
||||
return 0, false, err
|
||||
}
|
||||
return id, true, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return map[int64]domain.ProfilePhotoRef{}, nil
|
||||
}
|
||||
rows, err := s.q.CurrentProfilePhotosForOwners(ctx, sqlcgen.CurrentProfilePhotosForOwnersParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerIds: ownerIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(rows))
|
||||
for _, r := range rows {
|
||||
sizes, err := decodePhotoSizes(r.SizesJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[r.OwnerPeerID] = domain.ProfilePhotoRef{
|
||||
PhotoID: r.PhotoID,
|
||||
DCID: int(r.DcID),
|
||||
Stripped: domain.StrippedFromSizes(sizes),
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) ListProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, offset, limit int, maxID int64) ([]int64, int, error) {
|
||||
ids, err := s.q.ListProfilePhotos(ctx, sqlcgen.ListProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
MaxID: maxID,
|
||||
OffsetCount: int32(offset),
|
||||
LimitCount: int32(limit),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total, err := s.q.CountProfilePhotos(ctx, sqlcgen.CountProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ids, int(total), nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) DeleteProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) ([]int64, error) {
|
||||
if len(photoIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.q.DeactivateProfilePhotos(ctx, sqlcgen.DeactivateProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
PhotoIds: photoIDs,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue