Paperclip/pkg/internal/services/attachments.go

263 lines
6.6 KiB
Go
Raw Normal View History

2024-05-17 07:59:51 +00:00
package services
import (
"context"
2024-05-17 07:59:51 +00:00
"fmt"
2024-10-27 05:13:40 +00:00
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
2024-08-20 09:08:40 +00:00
"math"
2024-05-17 07:59:51 +00:00
"mime"
"mime/multipart"
"net/http"
"path/filepath"
"time"
2024-05-17 07:59:51 +00:00
"github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/marshaler"
"github.com/eko/gocache/lib/v4/store"
2024-09-11 15:55:46 +00:00
"github.com/spf13/viper"
"gorm.io/datatypes"
2024-11-02 02:41:31 +00:00
localCache "git.solsynth.dev/hypernet/paperclip/pkg/internal/cache"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
2024-07-25 14:02:26 +00:00
2024-11-02 02:41:31 +00:00
"git.solsynth.dev/hypernet/paperclip/pkg/internal/models"
2024-05-17 07:59:51 +00:00
"github.com/google/uuid"
"gorm.io/gorm"
)
func GetAttachmentCacheKey(rid string) any {
return fmt.Sprintf("attachment#%s", rid)
}
2024-06-29 13:16:11 +00:00
2024-05-17 07:59:51 +00:00
func GetAttachmentByID(id uint) (models.Attachment, error) {
var attachment models.Attachment
2024-09-11 15:55:46 +00:00
if err := database.C.
2024-10-27 05:13:40 +00:00
Where("id = ?", id).
2024-09-11 15:55:46 +00:00
Preload("Pool").Preload("Account").
First(&attachment).Error; err != nil {
2024-05-17 07:59:51 +00:00
return attachment, err
2024-06-29 13:16:11 +00:00
} else {
2024-08-18 11:53:50 +00:00
CacheAttachment(attachment)
}
return attachment, nil
}
func GetAttachmentByRID(rid string) (models.Attachment, error) {
cacheManager := cache.New[any](localCache.S)
marshal := marshaler.New(cacheManager)
contx := context.Background()
if val, err := marshal.Get(
contx,
GetAttachmentCacheKey(rid),
new(models.Attachment),
); err == nil {
if val.(models.Attachment).Account.ID > 0 {
return val.(models.Attachment), nil
}
2024-08-18 11:53:50 +00:00
}
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
Rid: rid,
}).Preload("Pool").Preload("Account").First(&attachment).Error; err != nil {
return attachment, err
} else {
CacheAttachment(attachment)
2024-05-17 07:59:51 +00:00
}
2024-06-29 13:16:11 +00:00
2024-05-17 07:59:51 +00:00
return attachment, nil
}
func GetAttachmentByHash(hash string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
HashCode: hash,
2024-08-18 06:09:52 +00:00
}).Preload("Pool").First(&attachment).Error; err != nil {
2024-05-17 07:59:51 +00:00
return attachment, err
}
return attachment, nil
}
func GetAttachmentCache(rid string) (models.Attachment, bool) {
cacheManager := cache.New[any](localCache.S)
marshal := marshaler.New(cacheManager)
contx := context.Background()
if val, err := marshal.Get(
contx,
GetAttachmentCacheKey(rid),
new(models.Attachment),
); err == nil {
if val.(models.Attachment).Account.ID > 0 {
return val.(models.Attachment), true
}
2024-08-06 15:45:58 +00:00
}
return models.Attachment{}, false
}
2024-08-18 11:53:50 +00:00
func CacheAttachment(item models.Attachment) {
cacheManager := cache.New[any](localCache.S)
marshal := marshaler.New(cacheManager)
contx := context.Background()
2024-10-27 05:13:40 +00:00
_ = marshal.Set(
contx,
GetAttachmentCacheKey(item.Rid),
item,
store.WithExpiration(60*time.Minute),
store.WithTags([]string{"attachment", fmt.Sprintf("user#%d", item.AccountID)}),
)
2024-08-06 17:02:03 +00:00
}
2024-10-27 05:13:40 +00:00
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
2024-05-17 07:59:51 +00:00
}
attachment.MimeType = http.DetectContentType(fileHeader)
2024-05-17 07:59:51 +00:00
}
}
if err := tx.Save(&attachment).Error; err != nil {
return attachment, fmt.Errorf("failed to save attachment record: %v", err)
2024-06-29 13:16:11 +00:00
} else {
2024-08-18 11:53:50 +00:00
CacheAttachment(attachment)
2024-05-17 07:59:51 +00:00
}
return attachment, nil
}
2024-10-27 05:13:40 +00:00
func NewAttachmentPlaceholder(tx *gorm.DB, user sec.UserInfo, attachment models.Attachment) (models.Attachment, error) {
2024-08-20 09:08:40 +00:00
attachment.Uuid = uuid.NewString()
attachment.Rid = RandString(16)
attachment.IsUploaded = false
attachment.FileChunks = datatypes.JSONMap{}
attachment.AccountID = user.ID
chunkSize := viper.GetInt64("performance.file_chunk_size")
chunkCount := math.Ceil(float64(attachment.Size) / float64(chunkSize))
for idx := 0; idx < int(chunkCount); idx++ {
cid := RandString(8)
attachment.FileChunks[cid] = idx
}
// 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)
}
}
if err := tx.Save(&attachment).Error; err != nil {
return attachment, fmt.Errorf("failed to save attachment record: %v", err)
} else {
CacheAttachment(attachment)
}
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
}
2024-10-20 06:12:36 +00:00
if prev.PoolID != nil && og.PoolID != nil && prev.PoolID != og.PoolID && prev.Pool != nil && og.Pool != nil {
2024-08-18 06:09:52 +00:00
if !prev.Pool.Config.Data().AllowCrossPoolEgress || !og.Pool.Config.Data().AllowCrossPoolIngress {
// Pool config doesn't allow reference
return false, nil
}
}
prev.RefCount++
og.RefID = &prev.ID
og.Uuid = prev.Uuid
og.Destination = prev.Destination
2024-08-10 17:08:08 +00:00
if og.AccountID == prev.AccountID {
og.IsSelfRef = true
}
if err := tx.Save(&og).Error; err != nil {
tx.Rollback()
return true, err
} else if err = tx.Save(&prev).Error; err != nil {
tx.Rollback()
return true, err
}
2024-08-18 11:53:50 +00:00
CacheAttachment(prev)
CacheAttachment(og)
return true, nil
2024-05-17 07:59:51 +00:00
}
2024-06-29 13:16:11 +00:00
func UpdateAttachment(item models.Attachment) (models.Attachment, error) {
if err := database.C.Updates(&item).Error; err != nil {
2024-06-29 13:16:11 +00:00
return item, err
} else {
2024-08-18 11:53:50 +00:00
CacheAttachment(item)
2024-06-29 13:16:11 +00:00
}
return item, nil
}
2024-05-17 07:59:51 +00:00
func DeleteAttachment(item models.Attachment) error {
dat := item
tx := database.C.Begin()
if item.RefID != nil {
var refTarget models.Attachment
2024-10-27 05:13:40 +00:00
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)
}
}
2024-05-17 07:59:51 +00:00
}
if err := database.C.Delete(&item).Error; err != nil {
tx.Rollback()
2024-05-17 07:59:51 +00:00
return err
2024-06-29 13:16:11 +00:00
} else {
cacheManager := cache.New[any](localCache.S)
marshal := marshaler.New(cacheManager)
contx := context.Background()
2024-10-27 05:13:40 +00:00
_ = marshal.Delete(contx, GetAttachmentCacheKey(item.Rid))
2024-05-17 07:59:51 +00:00
}
tx.Commit()
if dat.RefCount == 0 {
2024-07-28 16:53:40 +00:00
PublishDeleteFileTask(dat)
2024-05-17 07:59:51 +00:00
}
return nil
}