memos/store/resource.go
Aleksandr Baryshnikov fa17dce046
feat: pre-signed URL for S3 storage (#2855)
Adds automatically background refresh of all external links if they are belongs to the current blob (S3) storage. The feature is disabled by default in order to keep backward compatibility.

The background go-routine spawns once during startup and periodically signs and updates external links if that links belongs to current S3 storage.

The original idea was to sign external links on-demand, however, with current architecture it will require duplicated code in plenty of places. If do it, the changes will be quite invasive and in the end pointless: I believe, the architecture will be eventually updated to give more scalable way for pluggable storage. For example - Upload/Download interface without hard dependency on external link. There are stubs already, but I don't feel confident enough to change significant part of the application architecture.
2024-01-29 21:12:29 +08:00

122 lines
2.8 KiB
Go

package store
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/usememos/memos/internal/util"
)
const (
// thumbnailImagePath is the directory to store image thumbnails.
thumbnailImagePath = ".thumbnail_cache"
)
type Resource struct {
ID int32
ResourceName string
// Standard fields
CreatorID int32
CreatedTs int64
UpdatedTs int64
// Domain specific fields
Filename string
Blob []byte
InternalPath string
ExternalLink string
Type string
Size int64
MemoID *int32
}
type FindResource struct {
GetBlob bool
ID *int32
ResourceName *string
CreatorID *int32
Filename *string
MemoID *int32
HasRelatedMemo bool
Limit *int
Offset *int
}
type UpdateResource struct {
ID int32
ResourceName *string
UpdatedTs *int64
Filename *string
InternalPath *string
ExternalLink *string
MemoID *int32
Blob []byte
}
type DeleteResource struct {
ID int32
MemoID *int32
}
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {
if !util.ResourceNameMatcher.MatchString(create.ResourceName) {
return nil, errors.New("invalid resource name")
}
return s.driver.CreateResource(ctx, create)
}
func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) {
return s.driver.ListResources(ctx, find)
}
func (s *Store) GetResource(ctx context.Context, find *FindResource) (*Resource, error) {
resources, err := s.ListResources(ctx, find)
if err != nil {
return nil, err
}
if len(resources) == 0 {
return nil, nil
}
return resources[0], nil
}
func (s *Store) UpdateResource(ctx context.Context, update *UpdateResource) (*Resource, error) {
if update.ResourceName != nil && !util.ResourceNameMatcher.MatchString(*update.ResourceName) {
return nil, errors.New("invalid resource name")
}
return s.driver.UpdateResource(ctx, update)
}
func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) error {
resource, err := s.GetResource(ctx, &FindResource{ID: &delete.ID})
if err != nil {
return errors.Wrap(err, "failed to get resource")
}
if resource == nil {
return errors.Wrap(nil, "resource not found")
}
// Delete the local file.
if resource.InternalPath != "" {
resourcePath := filepath.FromSlash(resource.InternalPath)
if !filepath.IsAbs(resourcePath) {
resourcePath = filepath.Join(s.Profile.Data, resourcePath)
}
_ = os.Remove(resourcePath)
}
// Delete the thumbnail.
if util.HasPrefixes(resource.Type, "image/png", "image/jpeg") {
ext := filepath.Ext(resource.Filename)
thumbnailPath := filepath.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d%s", resource.ID, ext))
_ = os.Remove(thumbnailPath)
}
return s.driver.DeleteResource(ctx, delete)
}