Sharing: Refactor link entity and database columns #367 #18

Add missing fields in js model and rename fields for mode clarity. A link token can be valid for multiple shares.

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-06-28 14:52:26 +02:00
parent e6bb13240d
commit f2955fdefc
9 changed files with 80 additions and 78 deletions

View file

@ -40,6 +40,8 @@ export default class Link extends Model {
Slug: "",
Token: "",
Expires: 0,
Views: 0,
MaxViews: 0,
Password: "",
HasPassword: false,
CanComment: false,

View file

@ -33,10 +33,10 @@ func UpdateLink(c *gin.Context) {
link.SetSlug(f.ShareSlug)
link.MaxViews = f.MaxViews
link.ShareExpires = f.ShareExpires
link.LinkExpires = f.LinkExpires
if f.ShareToken != "" {
link.ShareToken = strings.ToLower(f.ShareToken)
if f.LinkToken != "" {
link.LinkToken = strings.ToLower(f.LinkToken)
}
if f.Password != "" {
@ -97,7 +97,7 @@ func CreateLink(c *gin.Context) {
link.SetSlug(f.ShareSlug)
link.MaxViews = f.MaxViews
link.ShareExpires = f.ShareExpires
link.LinkExpires = f.LinkExpires
if f.Password != "" {
if err := link.SetPassword(f.Password); err != nil {

View file

@ -30,9 +30,9 @@ func TestLinkAlbum(t *testing.T) {
assert.NotEmpty(t, link.LinkUID)
assert.NotEmpty(t, link.ShareUID)
assert.NotEmpty(t, link.ShareToken)
assert.NotEmpty(t, link.LinkToken)
assert.Equal(t, true, link.CanEdit)
assert.Equal(t, 0, link.ShareExpires)
assert.Equal(t, 0, link.LinkExpires)
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
})
@ -78,8 +78,8 @@ func TestLinkPhoto(t *testing.T) {
assert.NotEmpty(t, link.LinkUID)
assert.NotEmpty(t, link.ShareUID)
assert.NotEmpty(t, link.ShareToken)
assert.Equal(t, 0, link.ShareExpires)
assert.NotEmpty(t, link.LinkToken)
assert.Equal(t, 0, link.LinkExpires)
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
})
@ -119,8 +119,8 @@ func TestLinkLabel(t *testing.T) {
assert.NotEmpty(t, link.LinkUID)
assert.NotEmpty(t, link.ShareUID)
assert.NotEmpty(t, link.ShareToken)
assert.Equal(t, 0, link.ShareExpires)
assert.NotEmpty(t, link.LinkToken)
assert.Equal(t, 0, link.LinkExpires)
assert.False(t, link.CanComment)
assert.True(t, link.CanEdit)
})

View file

@ -15,9 +15,9 @@ func Shares(router *gin.RouterGroup) {
router.GET("/:token", func(c *gin.Context) {
conf := service.Config()
shareToken := c.Param("token")
token := c.Param("token")
links := entity.FindValidLinks(shareToken, "")
links := entity.FindValidLinks(token, "")
if len(links) == 0 {
log.Warn("share: invalid token")
@ -26,21 +26,21 @@ func Shares(router *gin.RouterGroup) {
}
clientConfig := conf.GuestConfig()
clientConfig.SiteUrl = fmt.Sprintf("%ss/%s", clientConfig.SiteUrl, shareToken)
clientConfig.SiteUrl = fmt.Sprintf("%ss/%s", clientConfig.SiteUrl, token)
c.HTML(http.StatusOK, "share.tmpl", gin.H{"config": clientConfig})
})
router.GET("/:token/:uid", func(c *gin.Context) {
router.GET("/:token/:share", func(c *gin.Context) {
conf := service.Config()
shareToken := c.Param("token")
share := c.Param("uid")
token := c.Param("token")
share := c.Param("share")
links := entity.FindValidLinks(shareToken, share)
links := entity.FindValidLinks(token, share)
if len(links) != 1 {
log.Warn("share: invalid token or uid")
log.Warn("share: invalid token or share")
c.Redirect(http.StatusTemporaryRedirect, "/")
return
}
@ -48,12 +48,12 @@ func Shares(router *gin.RouterGroup) {
uid := links[0].ShareUID
if uid != share {
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("/s/%s/%s", shareToken, uid))
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("/s/%s/%s", token, uid))
return
}
clientConfig := conf.GuestConfig()
clientConfig.SiteUrl = fmt.Sprintf("%ss/%s/%s", clientConfig.SiteUrl, shareToken, uid)
clientConfig.SiteUrl = fmt.Sprintf("%ss/%s/%s", clientConfig.SiteUrl, token, uid)
clientConfig.SitePreview = fmt.Sprintf("%s/preview", clientConfig.SiteUrl)
if a, err := query.AlbumByUID(uid); err == nil {

View file

@ -24,12 +24,12 @@ import (
// GET /s/:token/:uid/preview
// TODO: Proof of concept, needs refactoring.
func SharePreview(router *gin.RouterGroup) {
router.GET("/:token/:uid/preview", func(c *gin.Context) {
router.GET("/:token/:share/preview", func(c *gin.Context) {
conf := service.Config()
shareToken := c.Param("token")
shareUID := c.Param("uid")
links := entity.FindLinks(shareToken, shareUID)
token := c.Param("token")
share := c.Param("share")
links := entity.FindLinks(token, share)
if len(links) != 1 {
log.Warn("share: invalid token (preview)")
@ -45,17 +45,17 @@ func SharePreview(router *gin.RouterGroup) {
return
}
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, shareUID)
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, share)
yesterday := time.Now().Add(-24 * time.Hour)
if info, err := os.Stat(previewFilename); err != nil {
log.Debugf("share: creating new preview for %s", shareUID)
log.Debugf("share: creating new preview for %s", share)
} else if info.ModTime().After(yesterday) {
log.Debugf("share: using cached preview for %s", shareUID)
log.Debugf("share: using cached preview for %s", share)
c.File(previewFilename)
return
} else if err := os.Remove(previewFilename); err != nil {
log.Errorf("share: could not remove old preview of %s", shareUID)
log.Errorf("share: could not remove old preview of %s", share)
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
return
}
@ -63,7 +63,7 @@ func SharePreview(router *gin.RouterGroup) {
var f form.PhotoSearch
// Previews may only contain public content in shared albums.
f.Album = shareUID
f.Album = share
f.Public = true
f.Private = false
f.Hidden = false

View file

@ -15,18 +15,18 @@ type Links []Link
// Link represents a sharing link.
type Link struct {
LinkUID string `gorm:"type:varbinary(42);primary_key;" json:"UID,omitempty" yaml:"UID,omitempty"`
ShareUID string `gorm:"type:varbinary(42);unique_index:idx_links_uid_token;" json:"Share" yaml:"Share"`
ShareSlug string `gorm:"type:varbinary(255);index;" json:"Slug" yaml:"Slug,omitempty"`
ShareToken string `gorm:"type:varbinary(255);unique_index:idx_links_uid_token;" json:"Token" yaml:"Token,omitempty"`
ShareExpires int `json:"Expires" yaml:"Expires,omitempty"`
ShareViews uint `json:"ShareViews" yaml:"-"`
MaxViews uint `json:"MaxViews" yaml:"-"`
HasPassword bool `json:"HasPassword" yaml:"HasPassword,omitempty"`
CanComment bool `json:"CanComment" yaml:"CanComment,omitempty"`
CanEdit bool `json:"CanEdit" yaml:"CanEdit,omitempty"`
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt" yaml:"CreatedAt"`
ModifiedAt time.Time `deepcopier:"skip" yaml:"ModifiedAt"`
LinkUID string `gorm:"type:varbinary(42);primary_key;" json:"UID,omitempty" yaml:"UID,omitempty"`
ShareUID string `gorm:"type:varbinary(42);unique_index:idx_links_uid_token;" json:"Share" yaml:"Share"`
ShareSlug string `gorm:"type:varbinary(255);index;" json:"Slug" yaml:"Slug,omitempty"`
LinkToken string `gorm:"type:varbinary(255);unique_index:idx_links_uid_token;" json:"Token" yaml:"Token,omitempty"`
LinkExpires int `json:"Expires" yaml:"Expires,omitempty"`
LinkViews uint `json:"Views" yaml:"-"`
MaxViews uint `json:"MaxViews" yaml:"-"`
HasPassword bool `json:"HasPassword" yaml:"HasPassword,omitempty"`
CanComment bool `json:"CanComment" yaml:"CanComment,omitempty"`
CanEdit bool `json:"CanEdit" yaml:"CanEdit,omitempty"`
CreatedAt time.Time `deepcopier:"skip" json:"CreatedAt" yaml:"CreatedAt"`
ModifiedAt time.Time `deepcopier:"skip" yaml:"ModifiedAt"`
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
@ -45,7 +45,7 @@ func NewLink(shareUID string, canComment, canEdit bool) Link {
result := Link{
LinkUID: rnd.PPID('s'),
ShareUID: shareUID,
ShareToken: rnd.Token(10),
LinkToken: rnd.Token(10),
CanComment: canComment,
CanEdit: canEdit,
CreatedAt: now,
@ -56,9 +56,9 @@ func NewLink(shareUID string, canComment, canEdit bool) Link {
}
func (m *Link) Redeem() {
m.ShareViews += 1
m.LinkViews += 1
result := Db().Model(m).UpdateColumn("ShareViews", m.ShareViews)
result := Db().Model(m).UpdateColumn("LinkViews", m.LinkViews)
if result.RowsAffected == 0 {
log.Warnf("link: failed updating share view counter for %s", m.LinkUID)
@ -66,16 +66,16 @@ func (m *Link) Redeem() {
}
func (m *Link) Expired() bool {
if m.MaxViews > 0 && m.ShareViews >= m.MaxViews {
if m.MaxViews > 0 && m.LinkViews >= m.MaxViews {
return true
}
if m.ShareExpires <= 0 {
if m.LinkExpires <= 0 {
return false
}
now := Timestamp()
expires := m.ModifiedAt.Add(Seconds(m.ShareExpires))
expires := m.ModifiedAt.Add(Seconds(m.LinkExpires))
return now.Before(expires)
}
@ -116,7 +116,7 @@ func (m *Link) Save() error {
return fmt.Errorf("link: invalid share uid (%s)", m.ShareUID)
}
if m.ShareToken == "" {
if m.LinkToken == "" {
return fmt.Errorf("link: empty share token")
}
@ -127,7 +127,7 @@ func (m *Link) Save() error {
// Deletes the link.
func (m *Link) Delete() error {
if m.ShareToken == "" {
if m.LinkToken == "" {
return fmt.Errorf("link: empty share token")
}
@ -148,16 +148,16 @@ func FindLink(linkUID string) *Link {
}
// FindLinks returns a slice of links for a token and share UID (at least one must be provided).
func FindLinks(shareToken, share string) (result Links) {
if shareToken == "" && share == "" {
func FindLinks(token, share string) (result Links) {
if token == "" && share == "" {
log.Errorf("link: share token and uid must not be empty at the same time (find links)")
return []Link{}
}
q := Db()
if shareToken != "" {
q = q.Where("share_token = ?", shareToken)
if token != "" {
q = q.Where("link_token = ?", token)
}
if share != "" {
@ -176,8 +176,8 @@ func FindLinks(shareToken, share string) (result Links) {
}
// FindValidLinks returns a slice of non-expired links for a token and share UID (at least one must be provided).
func FindValidLinks(shareToken, shareUID string) (result Links) {
for _, link := range FindLinks(shareToken, shareUID) {
func FindValidLinks(token, share string) (result Links) {
for _, link := range FindLinks(token, share) {
if !link.Expired() {
result = append(result, link)
}
@ -188,5 +188,5 @@ func FindValidLinks(shareToken, shareUID string) (result Links) {
// String returns an human readable identifier for logging.
func (m *Link) String() string {
return fmt.Sprintf("%s/%s", m.ShareUID, m.ShareToken)
return m.LinkUID
}

View file

@ -8,12 +8,12 @@ type LinkMap map[string]Link
var LinkFixtures = LinkMap{
"1jxf3jfn2k": {
ShareToken: "1jxf3jfn2k",
ShareExpires: 0,
ShareUID: "st9lxuqxpogaaba7",
CanComment: true,
CanEdit: false,
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
LinkToken: "1jxf3jfn2k",
LinkExpires: 0,
ShareUID: "st9lxuqxpogaaba7",
CanComment: true,
CanEdit: false,
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
},
}

View file

@ -12,7 +12,7 @@ func TestNewLink(t *testing.T) {
assert.Equal(t, "st9lxuqxpogaaba1", link.ShareUID)
assert.Equal(t, false, link.CanEdit)
assert.Equal(t, true, link.CanComment)
assert.Equal(t, 10, len(link.ShareToken))
assert.Equal(t, 10, len(link.LinkToken))
assert.Equal(t, 16, len(link.LinkUID))
}
@ -22,20 +22,20 @@ func TestLink_Expired(t *testing.T) {
link := NewLink("st9lxuqxpogaaba1", true, false)
link.ModifiedAt = Timestamp().Add(-7 * Day)
link.ShareExpires = 0
link.LinkExpires = 0
assert.False(t, link.Expired())
link.ShareExpires = oneDay
link.LinkExpires = oneDay
assert.False(t, link.Expired())
link.ShareExpires = oneDay * 8
link.LinkExpires = oneDay * 8
assert.True(t, link.Expired())
link.ShareExpires = oneDay
link.ShareViews = 9
link.LinkExpires = oneDay
link.LinkViews = 9
link.MaxViews = 10
assert.False(t, link.Expired())
@ -48,11 +48,11 @@ func TestLink_Expired(t *testing.T) {
func TestLink_Redeem(t *testing.T) {
link := NewLink(rnd.PPID('a'), false, false)
assert.Equal(t, uint(0), link.ShareViews)
assert.Equal(t, uint(0), link.LinkViews)
link.Redeem()
assert.Equal(t, uint(1), link.ShareViews)
assert.Equal(t, uint(1), link.LinkViews)
if err := link.Save(); err != nil {
t.Fatal(err)
@ -60,5 +60,5 @@ func TestLink_Redeem(t *testing.T) {
link.Redeem()
assert.Equal(t, uint(2), link.ShareViews)
assert.Equal(t, uint(2), link.LinkViews)
}

View file

@ -2,11 +2,11 @@ package form
// Link represents a link sharing form.
type Link struct {
Password string `json:"Password"`
ShareSlug string `json:"Slug"`
ShareToken string `json:"Token"`
ShareExpires int `json:"Expires"`
MaxViews uint `json:"MaxViews"`
CanComment bool `json:"CanComment"`
CanEdit bool `json:"CanEdit"`
Password string `json:"Password"`
ShareSlug string `json:"Slug"`
LinkToken string `json:"Token"`
LinkExpires int `json:"Expires"`
MaxViews uint `json:"MaxViews"`
CanComment bool `json:"CanComment"`
CanEdit bool `json:"CanEdit"`
}