mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-09 17:44:59 +09:00
GO-4146 Merge remote-tracking branch 'origin/main' into GO-4146-new-spacestore
# Conflicts: # go.mod # go.sum
This commit is contained in:
commit
3aa814b02b
19 changed files with 374 additions and 93 deletions
|
@ -3,10 +3,6 @@ package bookmark
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -266,7 +262,7 @@ func (s *service) ContentUpdaters(spaceID string, url string, parseBlock bool) (
|
|||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hash, err := s.loadImage(spaceID, data.Title, data.ImageUrl)
|
||||
hash, err := s.loadImage(spaceID, getFileNameFromURL(url, "cover"), data.ImageUrl)
|
||||
if err != nil {
|
||||
log.Errorf("load image: %s", err)
|
||||
return
|
||||
|
@ -283,7 +279,7 @@ func (s *service) ContentUpdaters(spaceID string, url string, parseBlock bool) (
|
|||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hash, err := s.loadImage(spaceID, "", data.FaviconUrl)
|
||||
hash, err := s.loadImage(spaceID, getFileNameFromURL(url, "icon"), data.FaviconUrl)
|
||||
if err != nil {
|
||||
log.Errorf("load favicon: %s", err)
|
||||
return
|
||||
|
@ -358,55 +354,35 @@ func (s *service) fetcher(spaceID string, blockID string, params bookmark.FetchP
|
|||
return nil
|
||||
}
|
||||
|
||||
func getFileNameFromURL(url, filename string) string {
|
||||
u, err := uri.ParseURI(url)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if u.Hostname() == "" {
|
||||
return ""
|
||||
}
|
||||
var urlFileExt string
|
||||
lastPath := strings.Split(u.Path, "/")
|
||||
if len(lastPath) > 0 {
|
||||
urlFileExt = filepath.Ext(lastPath[len(lastPath)-1])
|
||||
}
|
||||
|
||||
source := strings.TrimPrefix(u.Hostname(), "www.")
|
||||
source = strings.ReplaceAll(source, ".", "_")
|
||||
if source != "" {
|
||||
source += "_"
|
||||
}
|
||||
source += filename + urlFileExt
|
||||
return source
|
||||
}
|
||||
|
||||
func (s *service) loadImage(spaceId string, title, url string) (hash string, err error) {
|
||||
uploader := s.fileUploaderFactory.NewUploader(spaceId, objectorigin.Bookmark())
|
||||
|
||||
tempDir := s.tempDirService.TempDir()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download image: %s", resp.Status)
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile(tempDir, "anytype_downloaded_file_*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
_, err = io.Copy(tmpFile, resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = tmpFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileName := strings.Split(filepath.Base(url), "?")[0]
|
||||
if value := resp.Header.Get("Content-Disposition"); value != "" {
|
||||
contentDisposition := strings.Split(value, "filename=")
|
||||
if len(contentDisposition) > 1 {
|
||||
fileName = strings.Trim(contentDisposition[1], "\"")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if title != "" {
|
||||
fileName = title
|
||||
}
|
||||
res := uploader.SetName(fileName).SetFile(tmpFile.Name()).SetImageKind(model.ImageKind_AutomaticallyAdded).Upload(ctx)
|
||||
res := uploader.SetName(title).SetUrl(url).SetImageKind(model.ImageKind_AutomaticallyAdded).Upload(ctx)
|
||||
return res.FileObjectId, res.Err
|
||||
}
|
||||
|
|
|
@ -159,6 +159,70 @@ func TestService_FetchBookmarkContent(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetFileNameFromURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
filename string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Valid URL with file extension, includes www",
|
||||
url: "https://www.example.com/path/image.png",
|
||||
filename: "myfile",
|
||||
want: "example_com_myfile.png",
|
||||
},
|
||||
{
|
||||
name: "Valid URL without file extension",
|
||||
url: "https://example.com/path/file",
|
||||
filename: "myfile",
|
||||
want: "example_com_myfile",
|
||||
},
|
||||
{
|
||||
name: "Trailing slash, no explicit file name in path",
|
||||
url: "http://www.example.org/folder/",
|
||||
filename: "test",
|
||||
want: "example_org_test",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
url: "vvv",
|
||||
filename: "file",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Empty path (no trailing slash)",
|
||||
url: "http://www.example.com",
|
||||
filename: "file",
|
||||
want: "example_com_file",
|
||||
},
|
||||
{
|
||||
name: "Complex domain without www",
|
||||
url: "https://sub.example.co.uk/path/report.pdf",
|
||||
filename: "report",
|
||||
want: "sub_example_co_uk_report.pdf",
|
||||
},
|
||||
{
|
||||
name: "URL with query parameters (should ignore queries)",
|
||||
url: "https://www.testsite.com/path/subpath/file.txt?key=value",
|
||||
filename: "newfile",
|
||||
want: "testsite_com_newfile.txt",
|
||||
},
|
||||
{
|
||||
name: "URL ends with a dot-based extension in path but no file name",
|
||||
url: "https://www.example.net/path/.htaccess",
|
||||
filename: "hidden",
|
||||
want: "example_net_hidden.htaccess",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getFileNameFromURL(tt.url, tt.filename)
|
||||
assert.Equal(t, tt.want, got, "Output filename should match the expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const testHtml = `<html><head>
|
||||
<title>Title</title>
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ func (p *Archive) Init(ctx *smartblock.InitContext) (err error) {
|
|||
if err = p.SmartBlock.Init(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
p.DisableLayouts()
|
||||
p.AddHook(p.updateObjects, smartblock.HookAfterApply)
|
||||
|
||||
return p.updateObjects(smartblock.ApplyInfo{})
|
||||
|
|
|
@ -46,7 +46,6 @@ func (p *Dashboard) Init(ctx *smartblock.InitContext) (err error) {
|
|||
if err = p.SmartBlock.Init(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
p.DisableLayouts()
|
||||
p.AddHook(p.updateObjects, smartblock.HookAfterApply)
|
||||
return p.updateObjects(smartblock.ApplyInfo{})
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ func (p *Page) Init(ctx *smartblock.InitContext) (err error) {
|
|||
migrateFilesToObjects(p, p.fileObjectService)(ctx.State)
|
||||
}
|
||||
|
||||
p.EnableLayouts()
|
||||
if p.isRelationDeleted(ctx) {
|
||||
// todo: move this to separate component
|
||||
go func() {
|
||||
|
|
|
@ -163,7 +163,7 @@ type SmartBlock interface {
|
|||
|
||||
SendEvent(msgs []*pb.EventMessage)
|
||||
ResetToVersion(s *state.State) (err error)
|
||||
DisableLayouts()
|
||||
EnableLayouts()
|
||||
EnabledRelationAsDependentObjects()
|
||||
AddHook(f HookCallback, events ...Hook)
|
||||
AddHookOnce(id string, f HookCallback, events ...Hook)
|
||||
|
@ -236,7 +236,7 @@ type smartBlock struct {
|
|||
lastDepDetails map[string]*domain.Details
|
||||
restrictions restriction.Restrictions
|
||||
isDeleted bool
|
||||
disableLayouts bool
|
||||
enableLayouts bool
|
||||
|
||||
includeRelationObjectsAsDependents bool // used by some clients
|
||||
|
||||
|
@ -616,8 +616,12 @@ func (sb *smartBlock) IsLocked() bool {
|
|||
return activeCount > 0
|
||||
}
|
||||
|
||||
func (sb *smartBlock) DisableLayouts() {
|
||||
sb.disableLayouts = true
|
||||
func (sb *smartBlock) EnableLayouts() {
|
||||
sb.enableLayouts = true
|
||||
}
|
||||
|
||||
func (sb *smartBlock) IsLayoutsEnabled() bool {
|
||||
return sb.enableLayouts
|
||||
}
|
||||
|
||||
func (sb *smartBlock) EnabledRelationAsDependentObjects() {
|
||||
|
@ -704,7 +708,7 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
|
|||
migrationVersionUpdated = s.MigrationVersion() != parent.MigrationVersion()
|
||||
}
|
||||
|
||||
msgs, act, err := state.ApplyState(sb.SpaceID(), s, !sb.disableLayouts)
|
||||
msgs, act, err := state.ApplyState(sb.SpaceID(), s, sb.enableLayouts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1058,7 +1062,7 @@ func (sb *smartBlock) StateAppend(f func(d state.Doc) (s *state.State, changes [
|
|||
sb.updateRestrictions()
|
||||
sb.injectDerivedDetails(s, sb.SpaceID(), sb.Type())
|
||||
sb.execHooks(HookBeforeApply, ApplyInfo{State: s})
|
||||
msgs, act, err := state.ApplyState(sb.SpaceID(), s, !sb.disableLayouts)
|
||||
msgs, act, err := state.ApplyState(sb.SpaceID(), s, sb.enableLayouts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1093,7 +1097,7 @@ func (sb *smartBlock) StateRebuild(d state.Doc) (err error) {
|
|||
d.(*state.State).SetParent(sb.Doc.(*state.State))
|
||||
// todo: make store diff
|
||||
sb.execHooks(HookBeforeApply, ApplyInfo{State: d.(*state.State)})
|
||||
msgs, _, err := state.ApplyState(sb.SpaceID(), d.(*state.State), !sb.disableLayouts)
|
||||
msgs, _, err := state.ApplyState(sb.SpaceID(), d.(*state.State), sb.enableLayouts)
|
||||
log.Infof("changes: stateRebuild: %d events", len(msgs))
|
||||
if err != nil {
|
||||
// can't make diff - reopen doc
|
||||
|
|
|
@ -245,10 +245,14 @@ func (st *SmartTest) SetObjectTypes(objectTypes []domain.TypeKey) {
|
|||
st.Doc.(*state.State).SetObjectTypeKeys(objectTypes)
|
||||
}
|
||||
|
||||
func (st *SmartTest) DisableLayouts() {
|
||||
func (st *SmartTest) EnableLayouts() {
|
||||
return
|
||||
}
|
||||
|
||||
func (st *SmartTest) IsLayoutsEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (st *SmartTest) SendEvent(msgs []*pb.EventMessage) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ func (s *SpaceView) Init(ctx *smartblock.InitContext) (err error) {
|
|||
}
|
||||
s.log = s.log.With("spaceId", spaceId)
|
||||
|
||||
s.DisableLayouts()
|
||||
info := spaceinfo.NewSpacePersistentInfoFromState(ctx.State)
|
||||
newInfo := spaceinfo.NewSpacePersistentInfo(spaceId)
|
||||
newInfo.SetAccountStatus(info.GetAccountStatus()).
|
||||
|
|
|
@ -1127,7 +1127,7 @@ func (s *State) FileRelationKeys() []domain.RelationKey {
|
|||
}
|
||||
if rel.Key == bundle.RelationKeyCoverId.String() {
|
||||
coverType := s.Details().GetInt64(bundle.RelationKeyCoverType)
|
||||
if (coverType == 1 || coverType == 4) && slice.FindPos(keys, domain.RelationKey(rel.Key)) == -1 {
|
||||
if (coverType == 1 || coverType == 4 || coverType == 5) && slice.FindPos(keys, domain.RelationKey(rel.Key)) == -1 {
|
||||
keys = append(keys, domain.RelationKey(rel.Key))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2943,6 +2943,23 @@ func TestState_FileRelationKeys(t *testing.T) {
|
|||
// then
|
||||
assert.Len(t, keys, 0)
|
||||
})
|
||||
t.Run("unsplash cover", func(t *testing.T) {
|
||||
// given
|
||||
s := &State{
|
||||
relationLinks: pbtypes.RelationLinks{
|
||||
{Key: bundle.RelationKeyCoverId.String()},
|
||||
},
|
||||
details: domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyCoverType: domain.Int64(5),
|
||||
}),
|
||||
}
|
||||
|
||||
// when
|
||||
keys := s.FileRelationKeys()
|
||||
|
||||
// then
|
||||
assert.Len(t, keys, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_AddRelationLinks(t *testing.T) {
|
||||
|
|
|
@ -335,6 +335,11 @@ func (s *source) buildState() (doc state.Doc, err error) {
|
|||
template.WithRelations([]domain.RelationKey{bundle.RelationKeyBacklinks})(st)
|
||||
}
|
||||
|
||||
if s.Type() == smartblock.SmartBlockTypeWidget {
|
||||
// todo: remove this after 0.41 release
|
||||
state.CleanupLayouts(st)
|
||||
}
|
||||
|
||||
s.fileObjectMigrator.MigrateFiles(st, s.space, s.GetFileKeysSnapshot())
|
||||
// Details in spaceview comes from Workspace object, so we don't need to migrate them
|
||||
if s.Type() != smartblock.SmartBlockTypeSpaceView {
|
||||
|
|
|
@ -217,13 +217,17 @@ func (h *MD) renderFile(buf writer, in *renderState, b *model.Block) {
|
|||
if file == nil || file.State != model.BlockContentFile_Done {
|
||||
return
|
||||
}
|
||||
name := escape.MarkdownCharacters(html.EscapeString(file.Name))
|
||||
title, filename, ok := h.getLinkInfo(file.TargetObjectId)
|
||||
if !ok {
|
||||
filename = h.fn.Get("files", file.TargetObjectId, filepath.Base(file.Name), filepath.Ext(file.Name))
|
||||
title = filepath.Base(file.Name)
|
||||
}
|
||||
buf.WriteString(in.indent)
|
||||
if file.Type != model.BlockContentFile_Image {
|
||||
fmt.Fprintf(buf, "[%s](%s) \n", name, h.fn.Get("files", file.TargetObjectId, filepath.Base(file.Name), filepath.Ext(file.Name)))
|
||||
fmt.Fprintf(buf, "[%s](%s) \n", title, filename)
|
||||
h.fileHashes = append(h.fileHashes, file.TargetObjectId)
|
||||
} else {
|
||||
fmt.Fprintf(buf, " \n", name, h.fn.Get("files", file.TargetObjectId, filepath.Base(file.Name), filepath.Ext(file.Name)))
|
||||
fmt.Fprintf(buf, " \n", title, filename)
|
||||
h.imageHashes = append(h.imageHashes, file.TargetObjectId)
|
||||
}
|
||||
}
|
||||
|
@ -394,12 +398,27 @@ func (h *MD) getLinkInfo(docId string) (title, filename string, ok bool) {
|
|||
return
|
||||
}
|
||||
title = info.GetString(bundle.RelationKeyName)
|
||||
// if object is a file
|
||||
layout := info.GetInt64(bundle.RelationKeyLayout)
|
||||
if layout == int64(model.ObjectType_file) || layout == int64(model.ObjectType_image) || layout == int64(model.ObjectType_audio) || layout == int64(model.ObjectType_video) {
|
||||
ext := info.GetString(bundle.RelationKeyFileExt)
|
||||
if ext != "" {
|
||||
ext = "." + ext
|
||||
}
|
||||
title = strings.TrimSuffix(title, ext)
|
||||
if title == "" {
|
||||
title = docId
|
||||
}
|
||||
filename = h.fn.Get("files", docId, title, ext)
|
||||
return
|
||||
}
|
||||
if title == "" {
|
||||
title = info.GetString(bundle.RelationKeySnippet)
|
||||
}
|
||||
if title == "" {
|
||||
title = docId
|
||||
}
|
||||
|
||||
filename = h.fn.Get("", docId, title, h.Ext())
|
||||
return
|
||||
}
|
||||
|
|
|
@ -272,7 +272,6 @@ func (u *uploader) SetUrl(url string) Uploader {
|
|||
if err != nil {
|
||||
// do nothing
|
||||
}
|
||||
u.SetName(strings.Split(filepath.Base(url), "?")[0])
|
||||
u.getReader = func(ctx context.Context) (*fileReader, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
|
@ -287,6 +286,10 @@ func (u *uploader) SetUrl(url string) Uploader {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("failed to download url, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var fileName string
|
||||
if content := resp.Header.Get("Content-Disposition"); content != "" {
|
||||
contentDisposition := strings.Split(content, "filename=")
|
||||
|
@ -294,6 +297,9 @@ func (u *uploader) SetUrl(url string) Uploader {
|
|||
fileName = strings.Trim(contentDisposition[1], "\"")
|
||||
}
|
||||
}
|
||||
if fileName == "" {
|
||||
fileName = uri.GetFileNameFromURLAndContentType(resp.Request.URL, resp.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile(u.tempDirProvider.TempDir(), "anytype_downloaded_file_*")
|
||||
if err != nil {
|
||||
|
@ -332,7 +338,10 @@ func (u *uploader) SetUrl(url string) Uploader {
|
|||
}
|
||||
|
||||
func (u *uploader) SetFile(path string) Uploader {
|
||||
u.SetName(filepath.Base(path))
|
||||
if u.name == "" {
|
||||
// only set name if it wasn't explicitly set before
|
||||
u.SetName(filepath.Base(path))
|
||||
}
|
||||
u.setLastModifiedDate(path)
|
||||
|
||||
u.getReader = func(ctx context.Context) (*fileReader, error) {
|
||||
|
@ -420,7 +429,12 @@ func (u *uploader) Upload(ctx context.Context) (result UploadResult) {
|
|||
}
|
||||
|
||||
if fileName := buf.GetFileName(); fileName != "" {
|
||||
u.SetName(fileName)
|
||||
if u.name == "" {
|
||||
u.SetName(fileName)
|
||||
} else if filepath.Ext(u.name) == "" {
|
||||
// enrich current name with extension
|
||||
u.name += filepath.Ext(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
if u.block != nil {
|
||||
|
|
11
go.mod
11
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/VividCortex/ewma v1.2.0
|
||||
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786
|
||||
github.com/anyproto/any-store v0.1.6
|
||||
github.com/anyproto/any-store v0.1.7
|
||||
github.com/anyproto/any-sync v0.6.0-alpha.11.0.20250131145459-0bf102738a87
|
||||
github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20250131145601-de288583ff2a
|
||||
github.com/anyproto/go-chash v0.1.0
|
||||
|
@ -31,7 +31,7 @@ require (
|
|||
github.com/ethereum/go-ethereum v1.13.15
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
|
||||
github.com/go-chi/chi/v5 v5.2.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-shiori/go-readability v0.0.0-20241012063810-92284fa8a71f
|
||||
github.com/goccy/go-graphviz v0.2.9
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
|
@ -104,19 +104,18 @@ require (
|
|||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/text v0.22.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20180125164251-1832d8546a9f
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
storj.io/drpc v0.0.34
|
||||
zombiezen.com/go/sqlite v1.4.0
|
||||
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -284,7 +283,7 @@ require (
|
|||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
|
|
18
go.sum
18
go.sum
|
@ -82,10 +82,10 @@ github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h
|
|||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/anyproto/any-store v0.1.6 h1:CmxIH2JhgHH2s7dnv1SNKAQ3spvX2amG/5RbrTZkLaQ=
|
||||
github.com/anyproto/any-store v0.1.6/go.mod h1:nbyRoJYOlvSWU1xDOrmgPP96UeoTf4eYZ9k+qqLK9k8=
|
||||
github.com/anyproto/any-sync v0.6.0-alpha.11.0.20250131145459-0bf102738a87 h1:Vkkz4b2e/ARu2NO2UhP6OxBUtrfBslVvtF9uCcYraRg=
|
||||
github.com/anyproto/any-sync v0.6.0-alpha.11.0.20250131145459-0bf102738a87/go.mod h1:JmjcChDSqgt6+G697YI3Yr57SZswxPJNX1arN//UYig=
|
||||
github.com/anyproto/any-store v0.1.7 h1:E3DntI+JXo3h7v0WTUJWH+nm7G4MV0PNOXZ6SFzQ2OU=
|
||||
github.com/anyproto/any-store v0.1.7/go.mod h1:nbyRoJYOlvSWU1xDOrmgPP96UeoTf4eYZ9k+qqLK9k8=
|
||||
github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20250131145601-de288583ff2a h1:ZZM+0OUCQMWSLSflpkf0ZMVo3V76qEDDIXPpQOClNs0=
|
||||
github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20250131145601-de288583ff2a/go.mod h1:4fkueCZcGniSMXkrwESO8zzERrh/L7WHimRNWecfGM0=
|
||||
github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 h1:Ba80IlCCxkZ9H1GF+7vFu/TSpPvbpDCxXJ5ogc4euYc=
|
||||
|
@ -323,8 +323,8 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
|
@ -1214,8 +1214,8 @@ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86h
|
|||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -1338,8 +1338,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -1453,8 +1454,9 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
|
@ -378,7 +378,7 @@ var (
|
|||
RelationKeyCoverType: {
|
||||
|
||||
DataSource: model.Relation_details,
|
||||
Description: "1-image, 2-color, 3-gradient, 4-prebuilt bg image. Value stored in coverId",
|
||||
Description: "1-image, 2-color, 3-gradient, 4-prebuilt bg image, 5 - unsplash image. Value stored in coverId",
|
||||
Format: model.RelationFormat_number,
|
||||
Hidden: true,
|
||||
Id: "_brcoverType",
|
||||
|
|
|
@ -574,34 +574,34 @@ func (_c *MockAccountObject_Details_Call) RunAndReturn(run func() *domain.Detail
|
|||
return _c
|
||||
}
|
||||
|
||||
// DisableLayouts provides a mock function with given fields:
|
||||
func (_m *MockAccountObject) DisableLayouts() {
|
||||
// EnableLayouts provides a mock function with given fields:
|
||||
func (_m *MockAccountObject) EnableLayouts() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockAccountObject_DisableLayouts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableLayouts'
|
||||
type MockAccountObject_DisableLayouts_Call struct {
|
||||
// MockAccountObject_EnableLayouts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableLayouts'
|
||||
type MockAccountObject_EnableLayouts_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DisableLayouts is a helper method to define mock.On call
|
||||
func (_e *MockAccountObject_Expecter) DisableLayouts() *MockAccountObject_DisableLayouts_Call {
|
||||
return &MockAccountObject_DisableLayouts_Call{Call: _e.mock.On("DisableLayouts")}
|
||||
// EnableLayouts is a helper method to define mock.On call
|
||||
func (_e *MockAccountObject_Expecter) EnableLayouts() *MockAccountObject_EnableLayouts_Call {
|
||||
return &MockAccountObject_EnableLayouts_Call{Call: _e.mock.On("EnableLayouts")}
|
||||
}
|
||||
|
||||
func (_c *MockAccountObject_DisableLayouts_Call) Run(run func()) *MockAccountObject_DisableLayouts_Call {
|
||||
func (_c *MockAccountObject_EnableLayouts_Call) Run(run func()) *MockAccountObject_EnableLayouts_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccountObject_DisableLayouts_Call) Return() *MockAccountObject_DisableLayouts_Call {
|
||||
func (_c *MockAccountObject_EnableLayouts_Call) Return() *MockAccountObject_EnableLayouts_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccountObject_DisableLayouts_Call) RunAndReturn(run func()) *MockAccountObject_DisableLayouts_Call {
|
||||
func (_c *MockAccountObject_EnableLayouts_Call) RunAndReturn(run func()) *MockAccountObject_EnableLayouts_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ package uri
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
@ -84,3 +86,68 @@ func NormalizeAndParseURI(uri string) (*url.URL, error) {
|
|||
|
||||
return url.Parse(normalizeURI(uri))
|
||||
}
|
||||
|
||||
var preferredExtensions = map[string]string{
|
||||
"image/jpeg": ".jpeg",
|
||||
"audio/mpeg": ".mp3",
|
||||
// Add more preferred mappings if needed
|
||||
}
|
||||
|
||||
func GetFileNameFromURLAndContentType(u *url.URL, contentType string) string {
|
||||
var host string
|
||||
if u != nil {
|
||||
|
||||
lastSegment := filepath.Base(u.Path)
|
||||
// Determine if this looks like a real filename. We'll say it's real if it has a dot or is a hidden file starting with a dot.
|
||||
if lastSegment == "." || lastSegment == "" || (!strings.HasPrefix(lastSegment, ".") && !strings.Contains(lastSegment, ".")) {
|
||||
// Not a valid filename
|
||||
lastSegment = ""
|
||||
}
|
||||
|
||||
if lastSegment != "" {
|
||||
// A plausible filename was found directly in the URL
|
||||
return lastSegment
|
||||
}
|
||||
|
||||
// No filename, fallback to host-based
|
||||
host = strings.TrimPrefix(u.Hostname(), "www.")
|
||||
host = strings.ReplaceAll(host, ".", "_")
|
||||
if host == "" {
|
||||
host = "file"
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get a preferred extension for the content type
|
||||
var ext string
|
||||
if preferred, ok := preferredExtensions[contentType]; ok {
|
||||
ext = preferred
|
||||
} else {
|
||||
extensions, err := mime.ExtensionsByType(contentType)
|
||||
if err != nil || len(extensions) == 0 {
|
||||
// Fallback if no known extension
|
||||
extensions = []string{".bin"}
|
||||
}
|
||||
ext = extensions[0]
|
||||
}
|
||||
|
||||
// Determine a base name from content type
|
||||
base := "file"
|
||||
if strings.HasPrefix(contentType, "image/") {
|
||||
base = "image"
|
||||
} else if strings.HasPrefix(contentType, "audio/") {
|
||||
base = "audio"
|
||||
} else if strings.HasPrefix(contentType, "video/") {
|
||||
base = "video"
|
||||
}
|
||||
|
||||
var res strings.Builder
|
||||
if host != "" {
|
||||
res.WriteString(host)
|
||||
res.WriteString("_")
|
||||
}
|
||||
res.WriteString(base)
|
||||
if ext != "" {
|
||||
res.WriteString(ext)
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package uri
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -118,3 +119,114 @@ func TestURI_ValidateURI(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFileNameFromURLWithContentTypeAndMime(t *testing.T) {
|
||||
mustParseURL := func(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse(%q) failed: %v", s, err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url *url.URL
|
||||
contentType string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "URL with explicit filename and extension",
|
||||
url: mustParseURL("https://example.com/image.jpg"),
|
||||
contentType: "image/jpeg",
|
||||
expected: "image.jpg",
|
||||
},
|
||||
{
|
||||
name: "URL with explicit filename and extension, but wrong content type",
|
||||
url: mustParseURL("https://example.com/image.jpg"),
|
||||
contentType: "image/png",
|
||||
expected: "image.jpg",
|
||||
},
|
||||
{
|
||||
name: "URL with explicit filename and extension, and empty content type",
|
||||
url: mustParseURL("https://example.com/image.jpg"),
|
||||
contentType: "",
|
||||
expected: "image.jpg",
|
||||
},
|
||||
{
|
||||
name: "URL with query and fragment, explicit filename",
|
||||
url: mustParseURL("https://example.com/file.jpeg?query=1#111"),
|
||||
contentType: "image/jpeg",
|
||||
expected: "file.jpeg",
|
||||
},
|
||||
{
|
||||
name: "No filename in URL, fallback to host and image/jpeg",
|
||||
url: mustParseURL("https://www.example.com/path/to/"),
|
||||
contentType: "image/jpeg",
|
||||
// host -> example_com
|
||||
// image/jpeg typically corresponds to .jpeg or .jpg (mime usually returns .jpeg)
|
||||
expected: "example_com_image.jpeg",
|
||||
},
|
||||
{
|
||||
name: "Host-only URL, fallback with image/png",
|
||||
url: mustParseURL("https://www.example.com"),
|
||||
contentType: "image/png",
|
||||
expected: "example_com_image.png",
|
||||
},
|
||||
{
|
||||
name: "Filename present with video/mp4",
|
||||
url: mustParseURL("https://www.sub.example.co.uk/folder/video.mp4"),
|
||||
contentType: "video/mp4",
|
||||
expected: "video.mp4",
|
||||
},
|
||||
{
|
||||
name: "No extension but filename present",
|
||||
url: mustParseURL("https://example.com/filename"),
|
||||
contentType: "image/gif",
|
||||
expected: "example_com_image.gif",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL returns empty",
|
||||
url: nil,
|
||||
contentType: "image/jpeg",
|
||||
expected: "image.jpeg",
|
||||
},
|
||||
{
|
||||
name: "No filename, video/unknown fallback to .bin",
|
||||
url: mustParseURL("https://www.subdomain.example.com/folder/"),
|
||||
contentType: "video/unknown",
|
||||
// no known extension for "video/unknown", fallback .bin
|
||||
expected: "subdomain_example_com_video.bin",
|
||||
},
|
||||
{
|
||||
name: "Hidden file as filename",
|
||||
url: mustParseURL("https://example.com/.htaccess"),
|
||||
contentType: "text/plain",
|
||||
expected: ".htaccess",
|
||||
},
|
||||
{
|
||||
name: "URL with query but no filename extension, fallback audio/mpeg",
|
||||
url: mustParseURL("https://example.com/path?version=2"),
|
||||
contentType: "audio/mpeg",
|
||||
// audio/mpeg known extension: .mp3
|
||||
expected: "example_com_audio.mp3",
|
||||
},
|
||||
{
|
||||
name: "Unknown type entirely",
|
||||
url: mustParseURL("https://example.net/"),
|
||||
contentType: "application/x-something-strange",
|
||||
// no filename, fallback host: example_net
|
||||
// unknown type -> .bin
|
||||
expected: "example_net_file.bin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetFileNameFromURLAndContentType(tt.url, tt.contentType)
|
||||
if got != tt.expected {
|
||||
t.Errorf("GetFileNameFromURL(%q, %q) = %q; want %q", tt.url, tt.contentType, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue