Paperclip/pkg/internal/services/attachments.go

328 lines
7.6 KiB
Go

package services
import (
"fmt"
"mime"
"mime/multipart"
"net/http"
"path/filepath"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
amodels "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"github.com/google/uuid"
"github.com/samber/lo"
"gorm.io/gorm"
)
func KgAttachmentCache(rid string) string {
return cachekit.FKey(cachekit.DAAttachment, rid)
}
func CompleteAttachmentMeta(in ...models.Attachment) ([]models.Attachment, error) {
var usersId []uint
for _, item := range in {
usersId = append(usersId, item.AccountID)
}
usersId = lo.Uniq(usersId)
users, err := authkit.ListUser(gap.Nx, usersId)
if err != nil {
return in, fmt.Errorf("failed to list users: %v", err)
}
for idx, item := range in {
item.Account = lo.FindOrElse(users, amodels.Account{}, func(idx amodels.Account) bool {
return item.AccountID == idx.ID
})
in[idx] = item
}
return in, nil
}
func GetAttachmentByID(id uint) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.
Where("id = ?", id).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
Preload("Boosts").
First(&attachment).Error; err != nil {
return attachment, err
} else {
CacheAttachment(attachment)
}
out, err := CompleteAttachmentMeta(attachment)
return out[0], err
}
func GetAttachmentByRID(rid string) (models.Attachment, error) {
if val, err := cachekit.Get[models.Attachment](
gap.Ca,
KgAttachmentCache(rid),
); err == nil {
return val, nil
}
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
Rid: rid,
}).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
Preload("Boosts").
First(&attachment).Error; err != nil {
return attachment, err
} else {
CacheAttachment(attachment)
}
out, err := CompleteAttachmentMeta(attachment)
return out[0], err
}
func GetAttachmentByHash(hash string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
HashCode: hash,
}).Preload("Pool").First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func GetAttachmentCache(rid string) (models.Attachment, bool) {
if val, err := cachekit.Get[models.Attachment](
gap.Ca,
KgAttachmentCache(rid),
); err == nil {
return val, true
}
return models.Attachment{}, false
}
func CacheAttachment(item models.Attachment) {
cachekit.Set[models.Attachment](
gap.Ca,
KgAttachmentCache(item.Rid),
item,
60*time.Minute,
)
}
func NewAttachmentMetadata(tx *gorm.DB, user *sec.UserInfo, file *multipart.FileHeader, attachment models.Attachment) (models.Attachment, error) {
attachment.Uuid = uuid.NewString()
attachment.Rid = RandString(16)
attachment.Size = file.Size
attachment.Name = file.Filename
attachment.AccountID = user.ID
// If the user didn't provide file mimetype manually, we have to detect it
if len(attachment.MimeType) == 0 {
if ext := filepath.Ext(attachment.Name); len(ext) > 0 {
// Detect mimetype by file extensions
attachment.MimeType = mime.TypeByExtension(ext)
} else {
// Detect mimetype by file header
// This method as a fallback method, because this isn't pretty accurate
header, err := file.Open()
if err != nil {
return attachment, fmt.Errorf("failed to read file header: %v", err)
}
defer header.Close()
fileHeader := make([]byte, 512)
_, err = header.Read(fileHeader)
if err != nil {
return attachment, err
}
attachment.MimeType = http.DetectContentType(fileHeader)
}
}
if err := tx.Save(&attachment).Error; err != nil {
return attachment, fmt.Errorf("failed to save attachment record: %v", err)
}
return attachment, nil
}
func TryLinkAttachment(tx *gorm.DB, og models.Attachment, hash string) (bool, error) {
prev, err := GetAttachmentByHash(hash)
if err != nil {
return false, err
}
if prev.PoolID != nil && og.PoolID != nil && prev.PoolID != og.PoolID && prev.Pool != nil && og.Pool != nil {
if !prev.Pool.Config.Data().AllowCrossPoolEgress || !og.Pool.Config.Data().AllowCrossPoolIngress {
// Pool config doesn't allow reference
return false, nil
}
}
if err := tx.Model(&og).Updates(&models.Attachment{
RefID: &prev.ID,
Uuid: prev.Uuid,
Destination: prev.Destination,
IsSelfRef: og.AccountID == prev.AccountID,
}).Error; err != nil {
tx.Rollback()
return true, err
} else if err = tx.Model(&prev).Update("ref_count", prev.RefCount+1).Error; err != nil {
tx.Rollback()
return true, err
}
return true, nil
}
func UpdateAttachment(item models.Attachment) (models.Attachment, error) {
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
return item, nil
}
func DeleteAttachment(item models.Attachment, txs ...*gorm.DB) error {
dat := item
var tx *gorm.DB
if len(txs) == 0 {
tx = database.C.Begin()
} else {
tx = txs[0]
}
if item.RefID != nil {
var refTarget models.Attachment
if err := database.C.Where("id = ?", *item.RefID).First(&refTarget).Error; err == nil {
refTarget.RefCount--
if err := tx.Save(&refTarget).Error; err != nil {
tx.Rollback()
return fmt.Errorf("unable to update ref count: %v", err)
}
}
}
if item.Thumbnail != nil {
if err := DeleteAttachment(*item.Thumbnail, tx); err != nil {
return err
}
}
if item.Compressed != nil {
if err := DeleteAttachment(*item.Compressed, tx); err != nil {
return err
}
}
if err := database.C.Delete(&item).Error; err != nil {
tx.Rollback()
return err
} else {
cachekit.Delete(gap.Ca, KgAttachmentCache(item.Rid))
}
tx.Commit()
if dat.RefCount == 0 {
go fs.DeleteFile(dat)
}
return nil
}
func DeleteAttachmentInBatch(items []models.Attachment, txs ...*gorm.DB) error {
if len(items) == 0 {
return nil
}
var tx *gorm.DB
if len(txs) == 0 {
tx = database.C.Begin()
} else {
tx = txs[0]
}
refIDs := []uint{}
for _, item := range items {
if item.RefID != nil {
refIDs = append(refIDs, *item.RefID)
}
}
if len(refIDs) > 0 {
var refTargets []models.Attachment
if err := tx.Where("id IN ?", refIDs).Find(&refTargets).Error; err == nil {
for i := range refTargets {
refTargets[i].RefCount--
}
if err := tx.Save(&refTargets).Error; err != nil {
tx.Rollback()
return fmt.Errorf("unable to update ref count: %v", err)
}
}
}
var subAttachments []models.Attachment
for _, item := range items {
if item.Thumbnail != nil {
subAttachments = append(subAttachments, *item.Thumbnail)
}
if item.Compressed != nil {
subAttachments = append(subAttachments, *item.Compressed)
}
}
if len(subAttachments) > 0 {
if err := DeleteAttachmentInBatch(subAttachments, tx); err != nil {
tx.Rollback()
return err
}
}
rids := make([]string, len(items))
for i, item := range items {
rids[i] = item.Rid
}
if err := tx.Where("rid IN ?", rids).Delete(&models.Attachment{}).Error; err != nil {
tx.Rollback()
return err
}
for _, rid := range rids {
cachekit.Delete(gap.Ca, KgAttachmentCache(rid))
}
tx.Commit()
go func() {
for _, item := range items {
if item.RefCount == 0 {
fs.DeleteFile(item)
}
}
}()
return nil
}
func CountAttachmentUsage(tx *gorm.DB, delta int) (int64, error) {
if tx := tx.Model(&models.Attachment{}).
Update("used_count", gorm.Expr("used_count + ?", delta)); tx.Error != nil {
return tx.RowsAffected, tx.Error
} else {
return tx.RowsAffected, nil
}
}