From bbf4462c6c79cf669e8aa82b928edbb0751ad35e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 28 May 2025 10:31:49 +0530 Subject: [PATCH] temp --- server/cmd/museum/main.go | 2 + server/ente/errors.go | 4 +- server/ente/public_file.go | 3 +- ....down.sql => 100_single_file_url.down.sql} | 0 ..._url.up.sql => 100_single_file_url.up.sql} | 0 server/pkg/api/file_url.go | 28 +++ server/pkg/controller/public_collection.go | 17 +- server/pkg/controller/public_file.go | 236 ++++++++++++++++++ server/pkg/repo/public/public_collection.go | 2 +- server/pkg/repo/public/public_file.go | 161 ++++++++++++ 10 files changed, 445 insertions(+), 8 deletions(-) rename server/migrations/{99_single_file_url.down.sql => 100_single_file_url.down.sql} (100%) rename server/migrations/{99_single_file_url.up.sql => 100_single_file_url.up.sql} (100%) create mode 100644 server/pkg/api/file_url.go create mode 100644 server/pkg/controller/public_file.go create mode 100644 server/pkg/repo/public/public_file.go diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 2ed0c2b6e1..47922970ef 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -440,6 +440,8 @@ func main() { privateAPI.GET("/files/preview/:fileID", fileHandler.GetThumbnail) privateAPI.GET("/files/preview/v2/:fileID", fileHandler.GetThumbnail) + privateAPI.POST("files/share-url", fileHandler.ShareUrl) + privateAPI.PUT("/files/data", fileHandler.PutFileData) privateAPI.PUT("/files/video-data", fileHandler.PutVideoData) privateAPI.POST("/files/data/status-diff", fileHandler.FileDataStatusDiff) diff --git a/server/ente/errors.go b/server/ente/errors.go index 4a5a02fb47..595b2f0417 100644 --- a/server/ente/errors.go +++ b/server/ente/errors.go @@ -97,8 +97,8 @@ var ErrUserDeleted = errors.New("user account has been deleted") // ErrLockUnavailable is thrown when a lock could not be acquired var ErrLockUnavailable = errors.New("could not acquire lock") -// ErrActiveLinkAlreadyExists is thrown when the collection already has active public link -var ErrActiveLinkAlreadyExists = errors.New("Collection already has active public link") +// ErrActiveLinkAlreadyExists is thrown when an active link already exists for entity +var ErrActiveLinkAlreadyExists = errors.New("link already exists for this entity") // ErrNotImplemented indicates that the action that we tried to perform is not // available at this museum instance. e.g. this could be something that is not diff --git a/server/ente/public_file.go b/server/ente/public_file.go index b1dea14984..4ea33edb2a 100644 --- a/server/ente/public_file.go +++ b/server/ente/public_file.go @@ -5,7 +5,7 @@ type CreateFileUrl struct { FileID int64 `json:"fileID" binding:"required"` } -// UpdateFileResponse represents a response to the UpdateFileRequest +// UpdateFileUrl .. type UpdateFileUrl struct { LinkID string `json:"linkID" binding:"required"` FileID int64 `json:"fileID" binding:"required"` @@ -34,6 +34,7 @@ type PublicFileUrlRow struct { PasswordInfo *PassWordInfo EnableDownload bool CreatedAt int64 + UpdatedAt int64 } type FileUrl struct { diff --git a/server/migrations/99_single_file_url.down.sql b/server/migrations/100_single_file_url.down.sql similarity index 100% rename from server/migrations/99_single_file_url.down.sql rename to server/migrations/100_single_file_url.down.sql diff --git a/server/migrations/99_single_file_url.up.sql b/server/migrations/100_single_file_url.up.sql similarity index 100% rename from server/migrations/99_single_file_url.up.sql rename to server/migrations/100_single_file_url.up.sql diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go new file mode 100644 index 0000000000..7032478dfc --- /dev/null +++ b/server/pkg/api/file_url.go @@ -0,0 +1,28 @@ +package api + +import ( + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/museum/pkg/utils/handler" + "github.com/ente-io/stacktrace" + "github.com/gin-gonic/gin" + "net/http" +) + +// Update updates already existing file +func (h *FileHandler) ShareUrl(c *gin.Context) { + enteApp := auth.GetApp(c) + userID := auth.GetUserID(c.Request.Header) + var file ente.CreateFileUrl + if err := c.ShouldBindJSON(&file); err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + + response, err := h.Controller.Update(c, userID, file, enteApp) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, response) +} diff --git a/server/pkg/controller/public_collection.go b/server/pkg/controller/public_collection.go index 7f51a46330..d2b2f39dcd 100644 --- a/server/pkg/controller/public_collection.go +++ b/server/pkg/controller/public_collection.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/ente-io/museum/pkg/controller/access" "github.com/ente-io/museum/pkg/repo/public" "github.com/ente-io/museum/ente" @@ -55,20 +56,28 @@ type PublicCollectionController struct { FileController *FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController PublicCollectionRepo *public.PublicCollectionRepository + PublicFileRepo *public.PublicFileRepository CollectionRepo *repo.CollectionRepository UserRepo *repo.UserRepository JwtSecret []byte } -func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) { +func (c *PublicCollectionController) CreateFileUrl(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { + + userID := auth.GetUserID(ctx.Request.Header) + if err := c.FileController.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ + ActorUserId: userID, + FileIDs: []int64{req.FileID}, + }); err != nil { + return nil, stacktrace.Propagate(err, "failed to verify file ownership") + } accessToken := shortuuid.New()[0:AccessTokenLength] - err := c.PublicCollectionRepo. - Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect, req.EnableJoin) + err := c.PublicFileRepo.Insert(ctx, req.FileID, userID, accessToken) if err != nil { if errors.Is(err, ente.ErrActiveLinkAlreadyExists) { collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID}) if err2 != nil { - return ente.PublicURL{}, stacktrace.Propagate(err2, "") + return nil, stacktrace.Propagate(err2, "") } if publicUrls, ok := collectionToPubUrlMap[req.CollectionID]; ok { if len(publicUrls) > 0 { diff --git a/server/pkg/controller/public_file.go b/server/pkg/controller/public_file.go new file mode 100644 index 0000000000..b48aee3384 --- /dev/null +++ b/server/pkg/controller/public_file.go @@ -0,0 +1,236 @@ +package controller + +import ( + "context" + "errors" + "fmt" + "github.com/ente-io/museum/ente" + enteJWT "github.com/ente-io/museum/ente/jwt" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/museum/pkg/utils/time" + "github.com/ente-io/stacktrace" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "github.com/lithammer/shortuuid/v3" + "github.com/sirupsen/logrus" +) + +func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) { + accessToken := shortuuid.New()[0:AccessTokenLength] + err := c.PublicCollectionRepo. + Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect, req.EnableJoin) + if err != nil { + if errors.Is(err, ente.ErrActiveLinkAlreadyExists) { + collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID}) + if err2 != nil { + return ente.PublicURL{}, stacktrace.Propagate(err2, "") + } + if publicUrls, ok := collectionToPubUrlMap[req.CollectionID]; ok { + if len(publicUrls) > 0 { + return publicUrls[0], nil + } + } + // ideally we should never reach here + return ente.PublicURL{}, stacktrace.NewError("Unexpected state") + } else { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + } + response := ente.PublicURL{ + URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken), + ValidTill: req.ValidTill, + DeviceLimit: req.DeviceLimit, + EnableDownload: true, + EnableCollect: req.EnableCollect, + PasswordEnabled: false, + } + return response, nil +} + +func (c *PublicCollectionController) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) { + return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID) +} + +func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) { + collection, err := c.GetPublicCollection(ctx, true) + if err != nil { + return ente.File{}, stacktrace.Propagate(err, "") + } + collectionOwnerID := collection.Owner.ID + // Do not let any update happen via public Url + file.ID = 0 + file.OwnerID = collectionOwnerID + file.UpdationTime = time.Microseconds() + file.IsDeleted = false + createdFile, err := c.FileController.Create(ctx, collectionOwnerID, file, ctx.Request.UserAgent(), app) + if err != nil { + return ente.File{}, stacktrace.Propagate(err, "") + } + + // Note: Stop sending email notification for public collection till + // we add in-app setting to enable/disable email notifications + //go c.EmailNotificationCtrl.OnFilesCollected(file.OwnerID) + return createdFile, nil +} + +// Disable all public accessTokens generated for the given cID till date. +func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error { + err := c.PublicCollectionRepo.DisableSharing(ctx, cID) + return stacktrace.Propagate(err, "") +} + +func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) { + publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID) + if err != nil { + return ente.PublicURL{}, err + } + if req.ValidTill != nil { + publicCollectionToken.ValidTill = *req.ValidTill + } + if req.DeviceLimit != nil { + publicCollectionToken.DeviceLimit = *req.DeviceLimit + } + if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil { + publicCollectionToken.PassHash = req.PassHash + publicCollectionToken.Nonce = req.Nonce + publicCollectionToken.OpsLimit = req.OpsLimit + publicCollectionToken.MemLimit = req.MemLimit + } else if req.DisablePassword != nil && *req.DisablePassword { + publicCollectionToken.PassHash = nil + publicCollectionToken.Nonce = nil + publicCollectionToken.OpsLimit = nil + publicCollectionToken.MemLimit = nil + } + if req.EnableDownload != nil { + publicCollectionToken.EnableDownload = *req.EnableDownload + } + if req.EnableCollect != nil { + publicCollectionToken.EnableCollect = *req.EnableCollect + } + if req.EnableJoin != nil { + publicCollectionToken.EnableJoin = *req.EnableJoin + } + err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + return ente.PublicURL{ + URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token), + DeviceLimit: publicCollectionToken.DeviceLimit, + ValidTill: publicCollectionToken.ValidTill, + EnableDownload: publicCollectionToken.EnableDownload, + EnableCollect: publicCollectionToken.EnableCollect, + EnableJoin: publicCollectionToken.EnableJoin, + PasswordEnabled: publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "", + Nonce: publicCollectionToken.Nonce, + MemLimit: publicCollectionToken.MemLimit, + OpsLimit: publicCollectionToken.OpsLimit, + }, nil +} + +// VerifyPassword verifies if the user has provided correct pw hash. If yes, it returns a signed jwt token which can be +// used by the client to pass in other requests for public collection. +// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force +// attack for guessing password. +func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) { + accessContext := auth.MustGetPublicAccessContext(ctx) + publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get public collection info") + } + if publicCollectionToken.PassHash == nil || *publicCollectionToken.PassHash == "" { + return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link") + } + if req.PassHash != *publicCollectionToken.PassHash { + return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link") + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{ + PassHash: req.PassHash, + ExpiryTime: time.NDaysFromNow(365), + }) + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(c.JwtSecret) + + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return &ente.VerifyPasswordResponse{ + JWTToken: tokenString, + }, nil +} + +func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error { + token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.PublicAlbumPasswordClaim{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil + } + return c.JwtSecret, nil + }) + if err != nil { + return stacktrace.Propagate(err, "JWT parsed failed") + } + claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim) + + if !ok { + return stacktrace.Propagate(errors.New("no claim in jwt token"), "") + } + if token.Valid && claims.PassHash == passwordHash { + return nil + } + return ente.ErrInvalidPassword +} + +func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error { + logger.Info("updating public collection on account deletion") + collectionIDs, err := c.PublicCollectionRepo.GetActivePublicTokenForUser(ctx, userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + logger.WithField("cIDs", collectionIDs).Info("disable public tokens due to account deletion") + for _, collectionID := range collectionIDs { + err = c.Disable(ctx, collectionID) + if err != nil { + return stacktrace.Propagate(err, "") + } + } + return nil +} + +// GetPublicCollection will return collection info for a public url. +// is mustAllowCollect is set to true but the underlying collection doesn't allow uploading +func (c *PublicCollectionController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) { + accessContext := auth.MustGetPublicAccessContext(ctx) + collection, err := c.CollectionRepo.Get(accessContext.CollectionID) + if err != nil { + return ente.Collection{}, stacktrace.Propagate(err, "") + } + if collection.IsDeleted { + return ente.Collection{}, stacktrace.Propagate(ente.ErrNotFound, "collection is deleted") + } + // hide redundant/private information + collection.Sharees = nil + collection.MagicMetadata = nil + publicURLsWithLimitedInfo := make([]ente.PublicURL, 0) + for _, publicUrl := range collection.PublicURLs { + publicURLsWithLimitedInfo = append(publicURLsWithLimitedInfo, ente.PublicURL{ + EnableDownload: publicUrl.EnableDownload, + EnableCollect: publicUrl.EnableCollect, + PasswordEnabled: publicUrl.PasswordEnabled, + Nonce: publicUrl.Nonce, + MemLimit: publicUrl.MemLimit, + OpsLimit: publicUrl.OpsLimit, + EnableJoin: publicUrl.EnableJoin, + }) + } + collection.PublicURLs = publicURLsWithLimitedInfo + if mustAllowCollect { + if len(publicURLsWithLimitedInfo) != 1 { + errorMsg := fmt.Sprintf("Unexpected number of public urls: %d", len(publicURLsWithLimitedInfo)) + return ente.Collection{}, stacktrace.Propagate(ente.NewInternalError(errorMsg), "") + } + if !publicURLsWithLimitedInfo[0].EnableCollect { + return ente.Collection{}, stacktrace.Propagate(&ente.ErrPublicCollectDisabled, "") + } + } + return collection, nil +} diff --git a/server/pkg/repo/public/public_collection.go b/server/pkg/repo/public/public_collection.go index 600c213c11..092ab377d9 100644 --- a/server/pkg/repo/public/public_collection.go +++ b/server/pkg/repo/public/public_collection.go @@ -123,7 +123,7 @@ func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, ac url string, reason string, details ente.AbuseReportDetails) error { _, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_abuse_report (share_id, ip, user_agent, url, reason, details) VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT ON CONSTRAINT unique_report_sid_ip_ua DO UPDATE SET (reason, details) = ($5, $6)`, + ON CONFLICT ON CONSTRAINT unique_report_public_collection_id_ip_ua DO UPDATE SET (reason, details) = ($5, $6)`, accessCtx.ID, accessCtx.IP, accessCtx.UserAgent, url, reason, details) return stacktrace.Propagate(err, "failed to record abuse report") } diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/public_file.go new file mode 100644 index 0000000000..5d59121fd1 --- /dev/null +++ b/server/pkg/repo/public/public_file.go @@ -0,0 +1,161 @@ +package public + +import ( + "context" + "database/sql" + "fmt" + "github.com/ente-io/museum/ente/base" + + "github.com/ente-io/museum/ente" + "github.com/ente-io/stacktrace" + "github.com/lib/pq" +) + +// PublicFileRepository defines the methods for inserting, updating and +// retrieving entities related to public file +type PublicFileRepository struct { + DB *sql.DB + albumHost string +} + +// NewPublicFileRepository .. +func NewPublicFileRepository(db *sql.DB, albumHost string) *PublicFileRepository { + if albumHost == "" { + albumHost = "https://albums.ente.io" + } + return &PublicFileRepository{ + DB: db, + albumHost: albumHost, + } +} + +func (pcr *PublicFileRepository) GetAlbumUrl(token string) string { + return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token) +} + +func (pcr *PublicFileRepository) Insert( + ctx context.Context, + fileID int64, + ownerID int64, + token string, +) error { + id, err := base.NewID("pft") + if err != nil { + return stacktrace.Propagate(err, "failed to generate new ID for public file token") + } + _, err = pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens + (id, file_id, owner_id, access_token) VALUES ($1, $2, $3, $4)`, + id, fileID, ownerID, token) + if err != nil && err.Error() == "pq: duplicate key value violates unique constraint \"public_access_token_unique_idx\"" { + return ente.ErrActiveLinkAlreadyExists + } + return stacktrace.Propagate(err, "failed to insert") +} + +func (pcr *PublicFileRepository) DisableSharing(ctx context.Context, fileID int64) error { + _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = true where + file_id = $1 and is_disabled = false`, fileID) + return stacktrace.Propagate(err, "failed to disable sharing") +} + +// GetCollectionToActivePublicURLMap will return map of collectionID to PublicURLs which are not disabled yet. +// Note: The url could be expired or deviceLimit is already reached +func (pcr *PublicFileRepository) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) { + rows, err := pcr.DB.QueryContext(ctx, `SELECT collection_id, access_token, valid_till, device_limit, enable_download, enable_collect, enable_join, pw_nonce, mem_limit, ops_limit FROM + public_collection_tokens WHERE collection_id = ANY($1) and is_disabled = FALSE`, + pq.Array(collectionIDs)) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + defer rows.Close() + result := make(map[int64][]ente.PublicURL, 0) + for _, cID := range collectionIDs { + result[cID] = make([]ente.PublicURL, 0) + } + for rows.Next() { + publicUrl := ente.PublicURL{} + var collectionID int64 + var accessToken string + var nonce *string + var opsLimit, memLimit *int64 + if err = rows.Scan(&collectionID, &accessToken, &publicUrl.ValidTill, &publicUrl.DeviceLimit, &publicUrl.EnableDownload, &publicUrl.EnableCollect, &publicUrl.EnableJoin, &nonce, &memLimit, &opsLimit); err != nil { + return nil, stacktrace.Propagate(err, "") + } + publicUrl.URL = pcr.GetAlbumUrl(accessToken) + if nonce != nil { + publicUrl.Nonce = nonce + publicUrl.MemLimit = memLimit + publicUrl.OpsLimit = opsLimit + publicUrl.PasswordEnabled = true + } + result[collectionID] = append(result[collectionID], publicUrl) + } + return result, nil +} + +// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID +// Note: The token could be expired or deviceLimit is already reached +func (pcr *PublicFileRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) { + row := pcr.DB.QueryRowContext(ctx, `SELECT id, collection_id, access_token, valid_till, device_limit, + is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download, enable_collect, enable_join FROM + public_collection_tokens WHERE collection_id = $1 and is_disabled = FALSE`, + collectionID) + + //defer rows.Close() + ret := ente.PublicCollectionToken{} + err := row.Scan(&ret.ID, &ret.CollectionID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit, + &ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload, &ret.EnableCollect, &ret.EnableJoin) + if err != nil { + return ente.PublicCollectionToken{}, stacktrace.Propagate(err, "") + } + return ret, nil +} + +// UpdatePublicCollectionToken will update the row for corresponding public collection token +func (pcr *PublicFileRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.PublicCollectionToken) error { + _, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET valid_till = $1, device_limit = $2, + pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7, enable_collect = $8, enable_join = $9 + where id = $10`, + pct.ValidTill, pct.DeviceLimit, pct.PassHash, pct.Nonce, pct.MemLimit, pct.OpsLimit, pct.EnableDownload, pct.EnableCollect, pct.EnableJoin, pct.ID) + return stacktrace.Propagate(err, "failed to update public collection token") +} + +func (pcr *PublicFileRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) { + panic("not implemented, refactor & public collection") +} + +func (pcr *PublicFileRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { + panic("not implemented, refactor & public collection") +} + +// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past +func (pcr *PublicFileRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) { + panic("not implemented, refactor & public collection") +} + +func (pcr *PublicFileRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.PublicFileUrlRow, error) { + row := pcr.DB.QueryRowContext(ctx, + `SELECT id, file_id, is_disabled, valid_till, device_limit, password_info, + created_at, updated_at + from public_file_tokens + where access_token = $1 +`, accessToken) + var result = ente.PublicFileUrlRow{} + err := row.Scan(&result.LinkID, &result.FileID, &result.IsDisabled, &result.ValidTill, &result.DeviceLimit, result.PasswordInfo, &result.CreatedAt, &result.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, ente.ErrNotFound + } + return nil, stacktrace.Propagate(err, "failed to get public file url summary by token") + } + return &result, nil +} + +// CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days +func (pcr *PublicFileRepository) CleanupAccessHistory(ctx context.Context) error { + _, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_collection_access_history WHERE share_id IN (SELECT id FROM public_collection_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`) + if err != nil { + return stacktrace.Propagate(err, "failed to clean up public collection access history") + } + return nil +}