From c94878e190170e7109d736ee95aecd9899813812 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 7 May 2025 10:51:17 +0530 Subject: [PATCH 01/57] Model for single file sharing --- server/ente/public_file.go | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 server/ente/public_file.go diff --git a/server/ente/public_file.go b/server/ente/public_file.go new file mode 100644 index 0000000000..b1dea14984 --- /dev/null +++ b/server/ente/public_file.go @@ -0,0 +1,48 @@ +package ente + +// CreateFileUrl represents an encrypted file in the system +type CreateFileUrl struct { + FileID int64 `json:"fileID" binding:"required"` +} + +// UpdateFileResponse represents a response to the UpdateFileRequest +type UpdateFileUrl struct { + LinkID string `json:"linkID" binding:"required"` + FileID int64 `json:"fileID" binding:"required"` + ValidTill *int64 `json:"validTill"` + DeviceLimit *int `json:"deviceLimit"` + PasswordInfo *PassWordInfo `json:"passHash"` + EnableDownload *bool `json:"enableDownload"` + DisablePassword *bool `json:"disablePassword"` +} + +type PassWordInfo struct { + PassHash string `json:"passHash" binding:"required"` + Nonce string `json:"nonce" binding:"required"` + MemLimit int64 `json:"memLimit" binding:"required"` + OpsLimit int64 `json:"opsLimit" binding:"required"` +} + +type PublicFileUrlRow struct { + LinkID string + OwnerID int64 + FileID int64 + Token string + DeviceLimit int + ValidTill int64 + IsDisabled bool + PasswordInfo *PassWordInfo + EnableDownload bool + CreatedAt int64 +} + +type FileUrl struct { + LinkID string `json:"linkID" binding:"required"` + OwnerID int64 `json:"ownerID" binding:"required"` + FileID int64 `json:"fileID" binding:"required"` + ValidTill int64 `json:"validTill"` + DeviceLimit int `json:"deviceLimit"` + PasswordEnabled bool `json:"passwordEnabled"` + EnableDownload bool `json:"enableDownload"` + CreatedAt int64 `json:"createdAt"` +} From daec225ef8ea96311212bd132174ffc985b65ec5 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 7 May 2025 12:16:10 +0530 Subject: [PATCH 02/57] Add DB Scheme for file URL --- server/migrations/99_single_file_url.down.sql | 23 +++++++ server/migrations/99_single_file_url.up.sql | 69 +++++++++++++++++++ server/pkg/controller/file-url/controller.go | 1 + 3 files changed, 93 insertions(+) create mode 100644 server/migrations/99_single_file_url.down.sql create mode 100644 server/migrations/99_single_file_url.up.sql create mode 100644 server/pkg/controller/file-url/controller.go diff --git a/server/migrations/99_single_file_url.down.sql b/server/migrations/99_single_file_url.down.sql new file mode 100644 index 0000000000..30e2211701 --- /dev/null +++ b/server/migrations/99_single_file_url.down.sql @@ -0,0 +1,23 @@ + + +ALTER TABLE public_abuse_report + DROP CONSTRAINT IF EXISTS check_share_id_xor_file_share_id, + DROP CONSTRAINT IF EXISTS fk_public_abuse_report_file_token_id; + + +DROP INDEX IF EXISTS unique_report_public_collection_id_ip_ua; +DROP INDEX IF EXISTS unique_report_public_file_id_ip_ua; + +ALTER TABLE public_abuse_report DROP CONSTRAINT IF EXISTS fk_public_abuse_report_file_token_id; + +ALTER TABLE public_abuse_report DROP COLUMN IF EXISTS file_share_id; + +ALTER TABLE public_abuse_report + ADD CONSTRAINT unique_report_sid_ip_ua + UNIQUE (share_id, ip, user_agent); + +CREATE INDEX IF NOT EXISTS public_abuse_share_id_idx + ON public_abuse_report (share_id); + +DROP TABLE IF EXISTS public_file_tokens_access_history; +DROP TABLE IF EXISTS public_file_tokens; diff --git a/server/migrations/99_single_file_url.up.sql b/server/migrations/99_single_file_url.up.sql new file mode 100644 index 0000000000..7b1729e9d8 --- /dev/null +++ b/server/migrations/99_single_file_url.up.sql @@ -0,0 +1,69 @@ + + +CREATE TABLE IF NOT EXISTS public_file_tokens +( + id text primary key, + file_id bigint NOT NULL, + owner_id bigint NOT NULL, + access_token text not null, + valid_till bigint not null DEFAULT 0, + device_limit int not null DEFAULT 0, + is_disabled bool not null DEFAULT FALSE, + enable_download bool not null DEFAULT TRUE, + password_info JSONB, + created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(), + updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds() +); + + +CREATE OR REPLACE TRIGGER update_public_file_tokens_updated_at + BEFORE UPDATE + ON public_file_tokens + FOR EACH ROW +EXECUTE PROCEDURE + trigger_updated_at_microseconds_column(); + + +CREATE TABLE IF NOT EXISTS public_file_tokens_access_history +( + id text NOT NULL, + ip text not null, + user_agent text not null, + created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(), + CONSTRAINT unique_access_id_ip_ua UNIQUE (id, ip, user_agent), + CONSTRAINT fk_public_file_history_token_id + FOREIGN KEY (id) + REFERENCES public_file_tokens (id) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS public_access_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE; +CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at); + + + +ALTER TABLE public_abuse_report DROP CONSTRAINT IF EXISTS unique_report_sid_ip_ua; +DROP INDEX IF EXISTS public_abuse_share_id_idx; + +ALTER TABLE public_abuse_report ADD COLUMN IF NOT EXISTS file_share_id text; +ALTER TABLE public_abuse_report + ADD CONSTRAINT fk_public_abuse_report_file_token_id + FOREIGN KEY (file_share_id) + REFERENCES public_file_tokens (id) + ON DELETE CASCADE; + +ALTER TABLE public_abuse_report + ADD CONSTRAINT check_share_id_xor_file_share_id + CHECK ( + (share_id IS NULL AND file_share_id IS NOT NULL) OR + (share_id IS NOT NULL AND file_share_id IS NULL) + ); + + +CREATE UNIQUE INDEX unique_report_public_collection_id_ip_ua + ON public_abuse_report (share_id, ip, user_agent) + WHERE share_id IS NOT NULL; + +CREATE UNIQUE INDEX unique_report_public_file_id_ip_ua + ON public_abuse_report (file_share_id, ip, user_agent) + WHERE file_share_id IS NOT NULL; \ No newline at end of file diff --git a/server/pkg/controller/file-url/controller.go b/server/pkg/controller/file-url/controller.go new file mode 100644 index 0000000000..7a53dc487c --- /dev/null +++ b/server/pkg/controller/file-url/controller.go @@ -0,0 +1 @@ +package file_url From b9b239c2074c73ff6d78f36550e127082fc6f518 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 12 May 2025 15:18:03 +0530 Subject: [PATCH 03/57] move refactor --- server/cmd/museum/main.go | 5 +++-- server/migrations/99_single_file_url.down.sql | 1 + server/pkg/controller/public_collection.go | 3 ++- server/pkg/middleware/access_token.go | 3 ++- server/pkg/repo/collection.go | 3 ++- server/pkg/repo/{ => public}/public_collection.go | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) rename server/pkg/repo/{ => public}/public_collection.go (99%) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 7070b8af61..45a37be1e2 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -6,6 +6,7 @@ import ( b64 "encoding/base64" "fmt" "github.com/ente-io/museum/pkg/controller/collections" + "github.com/ente-io/museum/pkg/repo/public" "net/http" "os" "os/signal" @@ -176,7 +177,7 @@ func main() { fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo} familyRepo := &repo.FamilyRepository{DB: db} trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo} - publicCollectionRepo := repo.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums")) + publicCollectionRepo := public.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums")) collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo, TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger} pushRepo := &repo.PushTokenRepository{DB: db} @@ -898,7 +899,7 @@ func setupAndStartBackgroundJobs( objectCleanupController.StartClearingOrphanObjects() } -func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *repo.PublicCollectionRepository, +func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *public.PublicCollectionRepository, twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController, taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController, trashController *controller.TrashController, pushController *controller.PushController, diff --git a/server/migrations/99_single_file_url.down.sql b/server/migrations/99_single_file_url.down.sql index 30e2211701..f2955333c7 100644 --- a/server/migrations/99_single_file_url.down.sql +++ b/server/migrations/99_single_file_url.down.sql @@ -9,6 +9,7 @@ DROP INDEX IF EXISTS unique_report_public_collection_id_ip_ua; DROP INDEX IF EXISTS unique_report_public_file_id_ip_ua; ALTER TABLE public_abuse_report DROP CONSTRAINT IF EXISTS fk_public_abuse_report_file_token_id; +ALTER TABLE public_abuse_report DROP CONSTRAINT IF EXISTS unique_report_public_file_id_ip_ua; ALTER TABLE public_abuse_report DROP COLUMN IF EXISTS file_share_id; diff --git a/server/pkg/controller/public_collection.go b/server/pkg/controller/public_collection.go index 022a08812e..7f51a46330 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/repo/public" "github.com/ente-io/museum/ente" enteJWT "github.com/ente-io/museum/ente/jwt" @@ -53,7 +54,7 @@ const ( type PublicCollectionController struct { FileController *FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController - PublicCollectionRepo *repo.PublicCollectionRepository + PublicCollectionRepo *public.PublicCollectionRepository CollectionRepo *repo.CollectionRepository UserRepo *repo.UserRepository JwtSecret []byte diff --git a/server/pkg/middleware/access_token.go b/server/pkg/middleware/access_token.go index 702af77db8..05ba354580 100644 --- a/server/pkg/middleware/access_token.go +++ b/server/pkg/middleware/access_token.go @@ -5,6 +5,7 @@ import ( "context" "crypto/sha256" "fmt" + "github.com/ente-io/museum/pkg/repo/public" "net/http" "github.com/ente-io/museum/ente" @@ -26,7 +27,7 @@ var whitelistedCollectionShareIDs = []int64{111} // AccessTokenMiddleware intercepts and authenticates incoming requests type AccessTokenMiddleware struct { - PublicCollectionRepo *repo.PublicCollectionRepository + PublicCollectionRepo *public.PublicCollectionRepository PublicCollectionCtrl *controller.PublicCollectionController CollectionRepo *repo.CollectionRepository Cache *cache.Cache diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go index 3f9af70268..40acc98b8a 100644 --- a/server/pkg/repo/collection.go +++ b/server/pkg/repo/collection.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/ente-io/museum/pkg/repo/public" "strconv" t "time" @@ -24,7 +25,7 @@ import ( type CollectionRepository struct { DB *sql.DB FileRepo *FileRepository - PublicCollectionRepo *PublicCollectionRepository + PublicCollectionRepo *public.PublicCollectionRepository TrashRepo *TrashRepository SecretEncryptionKey []byte QueueRepo *QueueRepository diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public/public_collection.go similarity index 99% rename from server/pkg/repo/public_collection.go rename to server/pkg/repo/public/public_collection.go index f5ae8f2d72..600c213c11 100644 --- a/server/pkg/repo/public_collection.go +++ b/server/pkg/repo/public/public_collection.go @@ -1,4 +1,4 @@ -package repo +package public import ( "context" 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 04/57] 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 +} From 99f4d4ca4db8227d60366a937151f95474436752 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:56:12 +0530 Subject: [PATCH 05/57] Update schema --- server/migrations/102_single_file_url.up.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/migrations/102_single_file_url.up.sql b/server/migrations/102_single_file_url.up.sql index 3feae46c7f..5a1f55089a 100644 --- a/server/migrations/102_single_file_url.up.sql +++ b/server/migrations/102_single_file_url.up.sql @@ -10,7 +10,10 @@ CREATE TABLE IF NOT EXISTS public_file_tokens device_limit int not null DEFAULT 0, is_disabled bool not null DEFAULT FALSE, enable_download bool not null DEFAULT TRUE, - password_info JSONB, + pw_hash TEXT, + pw_nonce TEXT, + mem_limit BIGINT, + ops_limit BIGINT, created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(), updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds() ); From 46ba71a15a926332abbb6972df39e4879ae5326d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:12:33 +0530 Subject: [PATCH 06/57] Fix queries --- server/ente/public_file.go | 45 ++++--- server/pkg/controller/public_file.go | 7 +- server/pkg/repo/public/public_collection.go | 2 +- server/pkg/repo/public/public_file.go | 129 +++++++++----------- 4 files changed, 90 insertions(+), 93 deletions(-) diff --git a/server/ente/public_file.go b/server/ente/public_file.go index 4ea33edb2a..bce96ac274 100644 --- a/server/ente/public_file.go +++ b/server/ente/public_file.go @@ -7,20 +7,16 @@ type CreateFileUrl struct { // UpdateFileUrl .. type UpdateFileUrl struct { - LinkID string `json:"linkID" binding:"required"` - FileID int64 `json:"fileID" binding:"required"` - ValidTill *int64 `json:"validTill"` - DeviceLimit *int `json:"deviceLimit"` - PasswordInfo *PassWordInfo `json:"passHash"` - EnableDownload *bool `json:"enableDownload"` - DisablePassword *bool `json:"disablePassword"` -} - -type PassWordInfo struct { - PassHash string `json:"passHash" binding:"required"` - Nonce string `json:"nonce" binding:"required"` - MemLimit int64 `json:"memLimit" binding:"required"` - OpsLimit int64 `json:"opsLimit" binding:"required"` + LinkID string `json:"linkID" binding:"required"` + FileID int64 `json:"fileID" binding:"required"` + ValidTill *int64 `json:"validTill"` + DeviceLimit *int `json:"deviceLimit"` + PassHash *string + Nonce *string + MemLimit *int64 + OpsLimit *int64 + EnableDownload *bool `json:"enableDownload"` + DisablePassword *bool `json:"disablePassword"` } type PublicFileUrlRow struct { @@ -31,7 +27,10 @@ type PublicFileUrlRow struct { DeviceLimit int ValidTill int64 IsDisabled bool - PasswordInfo *PassWordInfo + PassHash *string + Nonce *string + MemLimit *int64 + OpsLimit *int64 EnableDownload bool CreatedAt int64 UpdatedAt int64 @@ -39,11 +38,23 @@ type PublicFileUrlRow struct { type FileUrl struct { LinkID string `json:"linkID" binding:"required"` + URL string `json:"url" binding:"required"` OwnerID int64 `json:"ownerID" binding:"required"` FileID int64 `json:"fileID" binding:"required"` ValidTill int64 `json:"validTill"` DeviceLimit int `json:"deviceLimit"` PasswordEnabled bool `json:"passwordEnabled"` - EnableDownload bool `json:"enableDownload"` - CreatedAt int64 `json:"createdAt"` + // Nonce contains the nonce value for the password if the link is password protected. + Nonce *string `json:"nonce,omitempty"` + MemLimit *int64 `json:"memLimit,omitempty"` + OpsLimit *int64 `json:"opsLimit,omitempty"` + EnableDownload bool `json:"enableDownload"` + CreatedAt int64 `json:"createdAt"` +} + +type PublicFileAccessContext struct { + ID int64 + IP string + UserAgent string + CollectionID int64 } diff --git a/server/pkg/controller/public_file.go b/server/pkg/controller/public_file.go index ee8148743b..d4e8d80346 100644 --- a/server/pkg/controller/public_file.go +++ b/server/pkg/controller/public_file.go @@ -25,11 +25,12 @@ type PublicFileController struct { func (c *PublicFileController) CreateFileUrl(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { actorUserID := auth.GetUserID(ctx.Request.Header) accessToken := shortuuid.New()[0:AccessTokenLength] - err := c.PublicFileRepo.Insert(ctx, req.FileID, actorUserID, accessToken) + id, err := c.PublicFileRepo.Insert(ctx, req.FileID, actorUserID, accessToken) if err == nil { return &ente.FileUrl{ - LinkID: accessToken, - FileID: req.FileID, + LinkID: *id, + FileID: req.FileID, + OwnerID: actorUserID, }, nil } return nil, stacktrace.NewError("This endpoint is deprecated. Please use CreatePublicCollectionToken instead") diff --git a/server/pkg/repo/public/public_collection.go b/server/pkg/repo/public/public_collection.go index 092ab377d9..600c213c11 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_public_collection_id_ip_ua DO UPDATE SET (reason, details) = ($5, $6)`, + ON CONFLICT ON CONSTRAINT unique_report_sid_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 index 5d59121fd1..1ffd57c07d 100644 --- a/server/pkg/repo/public/public_file.go +++ b/server/pkg/repo/public/public_file.go @@ -8,7 +8,6 @@ import ( "github.com/ente-io/museum/ente" "github.com/ente-io/stacktrace" - "github.com/lib/pq" ) // PublicFileRepository defines the methods for inserting, updating and @@ -38,18 +37,21 @@ func (pcr *PublicFileRepository) Insert( fileID int64, ownerID int64, token string, -) error { +) (*string, error) { id, err := base.NewID("pft") if err != nil { - return stacktrace.Propagate(err, "failed to generate new ID for public file token") + return nil, 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 + if err != nil { + if err.Error() == "pq: duplicate key value violates unique constraint \"public_access_token_unique_idx\"" { + return nil, ente.ErrActiveLinkAlreadyExists + } + return nil, stacktrace.Propagate(err, "failed to insert") } - return stacktrace.Propagate(err, "failed to insert") + return id, nil } func (pcr *PublicFileRepository) DisableSharing(ctx context.Context, fileID int64) error { @@ -58,90 +60,51 @@ func (pcr *PublicFileRepository) DisableSharing(ctx context.Context, fileID int6 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) - } +func convertRowToFileUrl(rows *sql.Rows) ([]ente.FileUrl, error) { + var fileUrls []ente.FileUrl for rows.Next() { - publicUrl := ente.PublicURL{} - var collectionID int64 - var accessToken string + fileUrl := ente.FileUrl{} 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, "") + var memLimit, opsLimit *int64 + err := rows.Scan(&fileUrl.LinkID, &fileUrl.OwnerID, &fileUrl.FileID, &fileUrl.ValidTill, &fileUrl.DeviceLimit, &nonce, &memLimit, &opsLimit, &fileUrl.EnableDownload, &fileUrl.CreatedAt) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to scan public file url row") } - 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) + fileUrl.Nonce = nonce + fileUrl.MemLimit = memLimit + fileUrl.OpsLimit = opsLimit + fileUrls = append(fileUrls, fileUrl) } - return result, nil + return fileUrls, nil } -// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID +// GetActiveFileUrlToken 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`, +func (pcr *PublicFileRepository) GetActiveFileUrlToken(ctx context.Context, collectionID int64) (*ente.PublicFileUrlRow, error) { + row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit, + is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM + public_file_tokens WHERE file_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) + ret := ente.PublicFileUrlRow{} + err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit, + &ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload) if err != nil { - return ente.PublicCollectionToken{}, stacktrace.Propagate(err, "") + return nil, 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") + return &ret, nil } 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, + `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit 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) + err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt) if err != nil { if err == sql.ErrNoRows { return nil, ente.ErrNotFound @@ -151,11 +114,33 @@ func (pcr *PublicFileRepository) GetFileUrlRowByToken(ctx context.Context, acces return &result, nil } -// CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days +// UpdateLink will update the row for corresponding public file token +func (pcr *PublicFileRepository) UpdateLink(ctx context.Context, pct ente.PublicFileUrlRow) error { + _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2, + pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7 + where id = $8`, + pct.ValidTill, pct.DeviceLimit, pct.PassHash, pct.Nonce, pct.MemLimit, pct.OpsLimit, pct.EnableDownload, pct.LinkID) + return stacktrace.Propagate(err, "failed to update public file token") +} + +func (pcr *PublicFileRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) { + panic("not implemented, refactor & public file") +} + +func (pcr *PublicFileRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { + panic("not implemented, refactor & public file") +} + +// 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 file") +} + +// CleanupAccessHistory public_file_tokens_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)))`) + _, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_file_tokens_access_history WHERE id IN (SELECT id FROM public_file_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 stacktrace.Propagate(err, "failed to clean up public file access history") } return nil } From c5d9b2408f34eead2f295f60972f496e52c3b35a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:33:25 +0530 Subject: [PATCH 07/57] Implement all repo method --- server/ente/public_file.go | 1 - server/pkg/api/file.go | 2 +- server/pkg/controller/public_file.go | 46 +++++++++++---- server/pkg/repo/public/public_file.go | 85 ++++++++++++--------------- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/server/ente/public_file.go b/server/ente/public_file.go index bce96ac274..e8462eaf9a 100644 --- a/server/ente/public_file.go +++ b/server/ente/public_file.go @@ -35,7 +35,6 @@ type PublicFileUrlRow struct { CreatedAt int64 UpdatedAt int64 } - type FileUrl struct { LinkID string `json:"linkID" binding:"required"` URL string `json:"url" binding:"required"` diff --git a/server/pkg/api/file.go b/server/pkg/api/file.go index 66147224a5..1de2880de6 100644 --- a/server/pkg/api/file.go +++ b/server/pkg/api/file.go @@ -24,7 +24,7 @@ import ( // FileHandler exposes request handlers for all encrypted file related requests type FileHandler struct { Controller *controller.FileController - FileUrlCtrl *controller.PublicFileController + FileUrlCtrl *controller.PublicFileLinkController FileCopyCtrl *file_copy.FileCopyController FileDataCtrl *filedata.Controller } diff --git a/server/pkg/controller/public_file.go b/server/pkg/controller/public_file.go index d4e8d80346..b15d313c93 100644 --- a/server/pkg/controller/public_file.go +++ b/server/pkg/controller/public_file.go @@ -11,27 +11,51 @@ import ( "github.com/lithammer/shortuuid/v3" ) -// PublicFileController controls share collection operations -type PublicFileController struct { +// PublicFileLinkController controls share collection operations +type PublicFileLinkController struct { FileController *FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController PublicCollectionRepo *public.PublicCollectionRepository - PublicFileRepo *public.PublicFileRepository + FileLinkRepo *public.FileLinkRepository CollectionRepo *repo.CollectionRepository UserRepo *repo.UserRepository JwtSecret []byte } -func (c *PublicFileController) CreateFileUrl(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { +func (c *PublicFileLinkController) CreateFileUrl(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { actorUserID := auth.GetUserID(ctx.Request.Header) accessToken := shortuuid.New()[0:AccessTokenLength] - id, err := c.PublicFileRepo.Insert(ctx, req.FileID, actorUserID, accessToken) + _, err := c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken) if err == nil { - return &ente.FileUrl{ - LinkID: *id, - FileID: req.FileID, - OwnerID: actorUserID, - }, nil + row, rowErr := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID) + if rowErr != nil { + return nil, stacktrace.Propagate(rowErr, "failed to get active file url token") + } + return c.mapRowToFileUrl(ctx, row), nil + } + return nil, stacktrace.Propagate(err, "failed to create public file link") +} + +func (c *PublicFileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.PublicFileUrlRow) *ente.FileUrl { + app := auth.GetApp(ctx) + var url string + if app == ente.Locker { + url = c.FileLinkRepo.LockerFileLink(row.Token) + } else { + url = c.FileLinkRepo.PhotoLink(row.Token) + } + return &ente.FileUrl{ + LinkID: row.LinkID, + FileID: row.FileID, + URL: url, + OwnerID: row.OwnerID, + ValidTill: row.ValidTill, + DeviceLimit: row.DeviceLimit, + PasswordEnabled: row.PassHash != nil, + Nonce: row.Nonce, + OpsLimit: row.OpsLimit, + MemLimit: row.MemLimit, + EnableDownload: row.EnableDownload, + CreatedAt: row.CreatedAt, } - return nil, stacktrace.NewError("This endpoint is deprecated. Please use CreatePublicCollectionToken instead") } diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/public_file.go index 1ffd57c07d..9841e7edc2 100644 --- a/server/pkg/repo/public/public_file.go +++ b/server/pkg/repo/public/public_file.go @@ -10,29 +10,37 @@ import ( "github.com/ente-io/stacktrace" ) -// PublicFileRepository defines the methods for inserting, updating and +// FileLinkRepository defines the methods for inserting, updating and // retrieving entities related to public file -type PublicFileRepository struct { - DB *sql.DB - albumHost string +type FileLinkRepository struct { + DB *sql.DB + photoHost string + lockerHost string } -// NewPublicFileRepository .. -func NewPublicFileRepository(db *sql.DB, albumHost string) *PublicFileRepository { +// NewFileLinkRepo .. +func NewFileLinkRepo(db *sql.DB, albumHost string, lockerHost string) *FileLinkRepository { if albumHost == "" { albumHost = "https://albums.ente.io" } - return &PublicFileRepository{ + if lockerHost == "" { + lockerHost = "https://locker.ente.io" + } + return &FileLinkRepository{ DB: db, - albumHost: albumHost, + photoHost: albumHost, } } -func (pcr *PublicFileRepository) GetAlbumUrl(token string) string { - return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token) +func (pcr *FileLinkRepository) PhotoLink(token string) string { + return fmt.Sprintf("%s/?t=%s", pcr.photoHost, token) } -func (pcr *PublicFileRepository) Insert( +func (pcr *FileLinkRepository) LockerFileLink(token string) string { + return fmt.Sprintf("%s/?t=%s", pcr.lockerHost, token) +} + +func (pcr *FileLinkRepository) Insert( ctx context.Context, fileID int64, ownerID int64, @@ -54,39 +62,14 @@ func (pcr *PublicFileRepository) Insert( return id, nil } -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") -} - -func convertRowToFileUrl(rows *sql.Rows) ([]ente.FileUrl, error) { - var fileUrls []ente.FileUrl - for rows.Next() { - fileUrl := ente.FileUrl{} - var nonce *string - var memLimit, opsLimit *int64 - err := rows.Scan(&fileUrl.LinkID, &fileUrl.OwnerID, &fileUrl.FileID, &fileUrl.ValidTill, &fileUrl.DeviceLimit, &nonce, &memLimit, &opsLimit, &fileUrl.EnableDownload, &fileUrl.CreatedAt) - if err != nil { - return nil, stacktrace.Propagate(err, "failed to scan public file url row") - } - fileUrl.Nonce = nonce - fileUrl.MemLimit = memLimit - fileUrl.OpsLimit = opsLimit - fileUrls = append(fileUrls, fileUrl) - } - return fileUrls, nil -} - // GetActiveFileUrlToken will return ente.PublicCollectionToken for given collection ID // Note: The token could be expired or deviceLimit is already reached -func (pcr *PublicFileRepository) GetActiveFileUrlToken(ctx context.Context, collectionID int64) (*ente.PublicFileUrlRow, error) { +func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.PublicFileUrlRow, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit, is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM public_file_tokens WHERE file_id = $1 and is_disabled = FALSE`, - collectionID) + fileID) - //defer rows.Close() ret := ente.PublicFileUrlRow{} err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit, &ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload) @@ -96,7 +79,7 @@ func (pcr *PublicFileRepository) GetActiveFileUrlToken(ctx context.Context, coll return &ret, nil } -func (pcr *PublicFileRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.PublicFileUrlRow, error) { +func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.PublicFileUrlRow, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit created_at, updated_at @@ -115,7 +98,7 @@ func (pcr *PublicFileRepository) GetFileUrlRowByToken(ctx context.Context, acces } // UpdateLink will update the row for corresponding public file token -func (pcr *PublicFileRepository) UpdateLink(ctx context.Context, pct ente.PublicFileUrlRow) error { +func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.PublicFileUrlRow) error { _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2, pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7 where id = $8`, @@ -123,21 +106,31 @@ func (pcr *PublicFileRepository) UpdateLink(ctx context.Context, pct ente.Public return stacktrace.Propagate(err, "failed to update public file token") } -func (pcr *PublicFileRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) { - panic("not implemented, refactor & public file") +func (pcr *FileLinkRepository) GetUniqueAccessCount(ctx context.Context, linkId string) (int64, error) { + row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_file_tokens_access_history WHERE id = $1`, linkId) + var count int64 = 0 + err := row.Scan(&count) + if err != nil { + return -1, stacktrace.Propagate(err, "") + } + return count, nil } -func (pcr *PublicFileRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { - panic("not implemented, refactor & public file") +func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { + _, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens_access_history + (id, ip, user_agent) VALUES ($1, $2, $3) + ON CONFLICT ON CONSTRAINT unique_access_id_ip_ua DO NOTHING;`, + shareID, ip, ua) + return stacktrace.Propagate(err, "failed to record access history") } // 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) { +func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) { panic("not implemented, refactor & public file") } // CleanupAccessHistory public_file_tokens_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 { +func (pcr *FileLinkRepository) CleanupAccessHistory(ctx context.Context) error { _, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_file_tokens_access_history WHERE id IN (SELECT id FROM public_file_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 file access history") From 2e49f581c4814a38b679660107dc03355b1d8d1e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:10:37 +0530 Subject: [PATCH 08/57] File link token middleware --- server/cmd/museum/main.go | 4 +- server/ente/public_file.go | 10 +- server/pkg/controller/public_file.go | 2 +- .../{access_token.go => collection_token.go} | 14 +- server/pkg/middleware/file_link_token.go | 171 ++++++++++++++++++ server/pkg/repo/public/public_file.go | 24 ++- server/pkg/utils/auth/auth.go | 5 +- 7 files changed, 205 insertions(+), 25 deletions(-) rename server/pkg/middleware/{access_token.go => collection_token.go} (91%) create mode 100644 server/pkg/middleware/file_link_token.go diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index ad5b42cae3..90845fbba9 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -358,7 +358,7 @@ func main() { } authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController} - accessTokenMiddleware := middleware.AccessTokenMiddleware{ + collectionTokenMiddleware := middleware.CollectionTokenMiddleware{ PublicCollectionRepo: publicCollectionRepo, PublicCollectionCtrl: publicCollectionCtrl, CollectionRepo: collectionRepo, @@ -404,7 +404,7 @@ func main() { familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) publicCollectionAPI := server.Group("/public-collection") - publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer)) + publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionTokenMiddleware.Authenticate(urlSanitizer)) healthCheckHandler := &api.HealthCheckHandler{ DB: db, diff --git a/server/ente/public_file.go b/server/ente/public_file.go index e8462eaf9a..3b43ac0f43 100644 --- a/server/ente/public_file.go +++ b/server/ente/public_file.go @@ -19,7 +19,7 @@ type UpdateFileUrl struct { DisablePassword *bool `json:"disablePassword"` } -type PublicFileUrlRow struct { +type FileLinkRow struct { LinkID string OwnerID int64 FileID int64 @@ -52,8 +52,8 @@ type FileUrl struct { } type PublicFileAccessContext struct { - ID int64 - IP string - UserAgent string - CollectionID int64 + ID string + IP string + UserAgent string + FileID int64 } diff --git a/server/pkg/controller/public_file.go b/server/pkg/controller/public_file.go index b15d313c93..c74f1050df 100644 --- a/server/pkg/controller/public_file.go +++ b/server/pkg/controller/public_file.go @@ -36,7 +36,7 @@ func (c *PublicFileLinkController) CreateFileUrl(ctx *gin.Context, req ente.Crea return nil, stacktrace.Propagate(err, "failed to create public file link") } -func (c *PublicFileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.PublicFileUrlRow) *ente.FileUrl { +func (c *PublicFileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl { app := auth.GetApp(ctx) var url string if app == ente.Locker { diff --git a/server/pkg/middleware/access_token.go b/server/pkg/middleware/collection_token.go similarity index 91% rename from server/pkg/middleware/access_token.go rename to server/pkg/middleware/collection_token.go index 05ba354580..079f6f6f95 100644 --- a/server/pkg/middleware/access_token.go +++ b/server/pkg/middleware/collection_token.go @@ -25,8 +25,8 @@ import ( var passwordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"} var whitelistedCollectionShareIDs = []int64{111} -// AccessTokenMiddleware intercepts and authenticates incoming requests -type AccessTokenMiddleware struct { +// CollectionTokenMiddleware intercepts and authenticates incoming requests +type CollectionTokenMiddleware struct { PublicCollectionRepo *public.PublicCollectionRepository PublicCollectionCtrl *controller.PublicCollectionController CollectionRepo *repo.CollectionRepository @@ -35,10 +35,10 @@ type AccessTokenMiddleware struct { DiscordController *discord.DiscordController } -// AccessTokenAuthMiddleware returns a middle ware that extracts the `X-Auth-Access-Token` +// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token` // within the header of a request and uses it to validate the access token and set the // ente.PublicAccessContext with auth.PublicAccessKey as key -func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { +func (m *CollectionTokenMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { return func(c *gin.Context) { accessToken := auth.GetAccessToken(c) if accessToken == "" { @@ -113,7 +113,7 @@ func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *g c.Next() } } -func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error { +func (m *CollectionTokenMiddleware) validateOwnersSubscription(cID int64) error { userID, err := m.CollectionRepo.GetOwnerID(cID) if err != nil { return stacktrace.Propagate(err, "") @@ -121,7 +121,7 @@ func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error { return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, false) } -func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context, +func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, collectionSummary ente.PublicCollectionSummary, ip string, ua string) (bool, error) { // skip deviceLimit check & record keeping for requests via CF worker if network.IsCFWorkerIP(ip) { @@ -163,7 +163,7 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context, } // validatePassword will verify if the user is provided correct password for the public album -func (m *AccessTokenMiddleware) validatePassword(c *gin.Context, reqPath string, +func (m *CollectionTokenMiddleware) validatePassword(c *gin.Context, reqPath string, collectionSummary ente.PublicCollectionSummary) error { if array.StringInList(reqPath, passwordWhiteListedURLs) { return nil diff --git a/server/pkg/middleware/file_link_token.go b/server/pkg/middleware/file_link_token.go new file mode 100644 index 0000000000..9ff26d2533 --- /dev/null +++ b/server/pkg/middleware/file_link_token.go @@ -0,0 +1,171 @@ +package middleware + +import ( + "context" + "fmt" + "github.com/ente-io/museum/pkg/repo/public" + "net/http" + + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/discord" + "github.com/ente-io/museum/pkg/repo" + "github.com/ente-io/museum/pkg/utils/array" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/museum/pkg/utils/network" + "github.com/ente-io/museum/pkg/utils/time" + "github.com/ente-io/stacktrace" + "github.com/gin-gonic/gin" + "github.com/patrickmn/go-cache" + "github.com/sirupsen/logrus" +) + +var filePasswordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"} + +// FileLinkMiddleware intercepts and authenticates incoming requests +type FileLinkMiddleware struct { + FileLinkRepo *public.FileLinkRepository + PublicCollectionCtrl *controller.PublicCollectionController + CollectionRepo *repo.CollectionRepository + Cache *cache.Cache + BillingCtrl *controller.BillingController + DiscordController *discord.DiscordController +} + +// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token` +// within the header of a request and uses it to validate the access token and set the +// ente.PublicAccessContext with auth.PublicAccessKey as key +func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { + return func(c *gin.Context) { + accessToken := auth.GetAccessToken(c) + if accessToken == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing accessToken"}) + return + } + clientIP := network.GetClientIP(c) + userAgent := c.GetHeader("User-Agent") + + cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":") + cachedValue, cacheHit := m.Cache.Get(cacheKey) + var publicCollectionSummary *ente.FileLinkRow + var err error + if !cacheHit { + publicCollectionSummary, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + if publicCollectionSummary.IsDisabled { + c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "disabled token"}) + return + } + // validate if user still has active paid subscription + if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(publicCollectionSummary.OwnerID, true); err != nil { + logrus.WithError(err).Warn("failed to verify active paid subscription") + c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"}) + return + } + + // validate device limit + reached, err := m.isDeviceLimitReached(c, publicCollectionSummary, clientIP, userAgent) + if err != nil { + logrus.WithError(err).Error("failed to check device limit") + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"}) + return + } + if reached { + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "reached device limit"}) + return + } + } else { + publicCollectionSummary = cachedValue.(*ente.FileLinkRow) + } + + if publicCollectionSummary.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry + publicCollectionSummary.ValidTill < time.Microseconds() { + c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "expired token"}) + return + } + + // checks password protected public collection + if publicCollectionSummary.PassHash != nil && *publicCollectionSummary.PassHash != "" { + reqPath := urlSanitizer(c) + if err = m.validatePassword(c, reqPath, publicCollectionSummary); err != nil { + logrus.WithError(err).Warn("password validation failed") + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) + return + } + } + + if !cacheHit { + m.Cache.Set(cacheKey, publicCollectionSummary, cache.DefaultExpiration) + } + + c.Set(auth.FileLinkAccessKey, &ente.PublicFileAccessContext{ + ID: publicCollectionSummary.LinkID, + IP: clientIP, + UserAgent: userAgent, + FileID: publicCollectionSummary.FileID, + }) + c.Next() + } +} +func (m *FileLinkMiddleware) validateOwnersSubscription(cID int64) error { + userID, err := m.CollectionRepo.GetOwnerID(cID) + if err != nil { + return stacktrace.Propagate(err, "") + } + return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) +} + +func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context, + collectionSummary *ente.FileLinkRow, ip string, ua string) (bool, error) { + // skip deviceLimit check & record keeping for requests via CF worker + if network.IsCFWorkerIP(ip) { + return false, nil + } + + sharedID := collectionSummary.LinkID + hasAccessedInPast, err := m.FileLinkRepo.AccessedInPast(ctx, sharedID, ip, ua) + if err != nil { + return false, stacktrace.Propagate(err, "") + } + // if the device has accessed the url in the past, let it access it now as well, irrespective of device limit. + if hasAccessedInPast { + return false, nil + } + count, err := m.FileLinkRepo.GetUniqueAccessCount(ctx, sharedID) + if err != nil { + return false, stacktrace.Propagate(err, "failed to get unique access count") + } + + deviceLimit := int64(collectionSummary.DeviceLimit) + if deviceLimit == controller.DeviceLimitThreshold { + deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold + } + + if count >= controller.DeviceLimitWarningThreshold { + m.DiscordController.NotifyPotentialAbuse( + fmt.Sprintf("Album exceeds warning threshold: {FileID: %d, ShareID: %s}", + collectionSummary.FileID, collectionSummary.LinkID)) + } + + if deviceLimit > 0 && count >= deviceLimit { + return true, nil + } + err = m.FileLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua) + return false, stacktrace.Propagate(err, "failed to record access history") +} + +// validatePassword will verify if the user is provided correct password for the public album +func (m *FileLinkMiddleware) validatePassword(c *gin.Context, reqPath string, + fileLinkRow *ente.FileLinkRow) error { + if array.StringInList(reqPath, passwordWhiteListedURLs) { + return nil + } + accessTokenJWT := auth.GetAccessTokenJWT(c) + if accessTokenJWT == "" { + return ente.ErrAuthenticationRequired + } + return m.PublicCollectionCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash) +} diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/public_file.go index 9841e7edc2..022a6ba7b0 100644 --- a/server/pkg/repo/public/public_file.go +++ b/server/pkg/repo/public/public_file.go @@ -3,6 +3,7 @@ package public import ( "context" "database/sql" + "errors" "fmt" "github.com/ente-io/museum/ente/base" @@ -64,13 +65,13 @@ func (pcr *FileLinkRepository) Insert( // GetActiveFileUrlToken will return ente.PublicCollectionToken for given collection ID // Note: The token could be expired or deviceLimit is already reached -func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.PublicFileUrlRow, error) { +func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit, is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM public_file_tokens WHERE file_id = $1 and is_disabled = FALSE`, fileID) - ret := ente.PublicFileUrlRow{} + ret := ente.FileLinkRow{} err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit, &ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload) if err != nil { @@ -79,14 +80,14 @@ func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID return &ret, nil } -func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.PublicFileUrlRow, error) { +func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit created_at, updated_at from public_file_tokens where access_token = $1 `, accessToken) - var result = ente.PublicFileUrlRow{} + var result = ente.FileLinkRow{} err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt) if err != nil { if err == sql.ErrNoRows { @@ -98,7 +99,7 @@ func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessT } // UpdateLink will update the row for corresponding public file token -func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.PublicFileUrlRow) error { +func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.FileLinkRow) error { _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2, pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7 where id = $8`, @@ -116,7 +117,7 @@ func (pcr *FileLinkRepository) GetUniqueAccessCount(ctx context.Context, linkId return count, nil } -func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { +func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID string, ip string, ua string) error { _, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens_access_history (id, ip, user_agent) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT unique_access_id_ip_ua DO NOTHING;`, @@ -125,8 +126,15 @@ func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID } // AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past -func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) { - panic("not implemented, refactor & public file") +func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID string, ip string, ua string) (bool, error) { + row := pcr.DB.QueryRowContext(ctx, `select id from public_file_tokens_access_history where id =$1 and ip = $2 and user_agent = $3`, + shareID, ip, ua) + var tempID int64 + err := row.Scan(&tempID) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return true, stacktrace.Propagate(err, "failed to record access history") } // CleanupAccessHistory public_file_tokens_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days diff --git a/server/pkg/utils/auth/auth.go b/server/pkg/utils/auth/auth.go index 6f8091998b..e352d168f1 100644 --- a/server/pkg/utils/auth/auth.go +++ b/server/pkg/utils/auth/auth.go @@ -17,8 +17,9 @@ import ( ) const ( - PublicAccessKey = "X-Public-Access-ID" - CastContext = "X-Cast-Context" + PublicAccessKey = "X-Public-Access-ID" + FileLinkAccessKey = "X-Public-FileLink-Access-ID" + CastContext = "X-Cast-Context" ) // GenerateRandomBytes returns securely generated random bytes. From 8d108dc719f4c4597fcda380fc7937404f4b7883 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:39:20 +0530 Subject: [PATCH 09/57] Rename --- server/cmd/museum/main.go | 27 +++++++------- server/ente/jwt/jwt.go | 6 +-- server/ente/public_collection.go | 6 +-- server/pkg/api/collection.go | 4 +- server/pkg/api/file.go | 3 +- server/pkg/api/file_url.go | 2 +- server/pkg/api/public_collection.go | 3 +- .../pkg/controller/collections/collection.go | 25 +++++++------ server/pkg/controller/collections/share.go | 18 ++++----- .../collection_link.go} | 37 ++++++++++--------- .../{public_file.go => public/file_link.go} | 24 ++++++------ server/pkg/controller/public/link_common.go | 1 + server/pkg/middleware/collection_token.go | 9 +++-- server/pkg/middleware/file_link_token.go | 11 +++--- server/pkg/repo/public/public_collection.go | 10 ++--- server/pkg/repo/public/public_file.go | 2 +- 16 files changed, 97 insertions(+), 91 deletions(-) rename server/pkg/controller/{public_collection.go => public/collection_link.go} (86%) rename server/pkg/controller/{public_file.go => public/file_link.go} (64%) create mode 100644 server/pkg/controller/public/link_common.go diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 90845fbba9..ed731b51da 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -6,6 +6,7 @@ import ( b64 "encoding/base64" "fmt" "github.com/ente-io/museum/pkg/controller/collections" + publicCtrl "github.com/ente-io/museum/pkg/controller/public" "github.com/ente-io/museum/pkg/repo/public" "net/http" "os" @@ -300,7 +301,7 @@ func main() { UsageRepo: usageRepo, } - publicCollectionCtrl := &controller.PublicCollectionController{ + collectionLinkCtrl := &publicCtrl.CollectionLinkController{ FileController: fileController, EmailNotificationCtrl: emailNotificationCtrl, PublicCollectionRepo: publicCollectionRepo, @@ -310,16 +311,16 @@ func main() { } collectionController := &collections.CollectionController{ - CollectionRepo: collectionRepo, - EmailCtrl: emailNotificationCtrl, - AccessCtrl: accessCtrl, - PublicCollectionCtrl: publicCollectionCtrl, - UserRepo: userRepo, - FileRepo: fileRepo, - CastRepo: &castDb, - BillingCtrl: billingController, - QueueRepo: queueRepo, - TaskRepo: taskLockingRepo, + CollectionRepo: collectionRepo, + EmailCtrl: emailNotificationCtrl, + AccessCtrl: accessCtrl, + CollectionLinkController: collectionLinkCtrl, + UserRepo: userRepo, + FileRepo: fileRepo, + CastRepo: &castDb, + BillingCtrl: billingController, + QueueRepo: queueRepo, + TaskRepo: taskLockingRepo, } kexCtrl := &kexCtrl.Controller{ @@ -360,7 +361,7 @@ func main() { authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController} collectionTokenMiddleware := middleware.CollectionTokenMiddleware{ PublicCollectionRepo: publicCollectionRepo, - PublicCollectionCtrl: publicCollectionCtrl, + PublicCollectionCtrl: collectionLinkCtrl, CollectionRepo: collectionRepo, Cache: accessTokenCache, BillingCtrl: billingController, @@ -568,7 +569,7 @@ func main() { privateAPI.PUT("/collections/sharee-magic-metadata", collectionHandler.ShareeMagicMetadataUpdate) publicCollectionHandler := &api.PublicCollectionHandler{ - Controller: publicCollectionCtrl, + Controller: collectionLinkCtrl, FileCtrl: fileController, CollectionCtrl: collectionController, FileDataCtrl: fileDataCtrl, diff --git a/server/ente/jwt/jwt.go b/server/ente/jwt/jwt.go index 94cfa995f2..c4d210b66c 100644 --- a/server/ente/jwt/jwt.go +++ b/server/ente/jwt/jwt.go @@ -40,13 +40,13 @@ func (w WebCommonJWTClaim) Valid() error { return nil } -// PublicAlbumPasswordClaim refer to token granted post public album password verification -type PublicAlbumPasswordClaim struct { +// LinkPasswordClaim refer to token granted post link password verification +type LinkPasswordClaim struct { PassHash string `json:"passKey"` ExpiryTime int64 `json:"expiryTime"` } -func (c PublicAlbumPasswordClaim) Valid() error { +func (c LinkPasswordClaim) Valid() error { if c.ExpiryTime < time.Microseconds() { return errors.New("token expired") } diff --git a/server/ente/public_collection.go b/server/ente/public_collection.go index eb1bd8c385..f34c0bf2f1 100644 --- a/server/ente/public_collection.go +++ b/server/ente/public_collection.go @@ -40,8 +40,8 @@ type VerifyPasswordResponse struct { JWTToken string `json:"jwtToken"` } -// PublicCollectionToken represents row entity for public_collection_token table -type PublicCollectionToken struct { +// CollectionLinkRow represents row entity for public_collection_token table +type CollectionLinkRow struct { ID int64 CollectionID int64 Token string @@ -57,7 +57,7 @@ type PublicCollectionToken struct { EnableJoin bool } -func (p PublicCollectionToken) CanJoin() error { +func (p CollectionLinkRow) CanJoin() error { if p.IsDisabled { return NewBadRequestWithMessage("link disabled") } diff --git a/server/pkg/api/collection.go b/server/pkg/api/collection.go index 9318f5c329..d5f918c672 100644 --- a/server/pkg/api/collection.go +++ b/server/pkg/api/collection.go @@ -3,6 +3,7 @@ package api import ( "fmt" "github.com/ente-io/museum/pkg/controller/collections" + "github.com/ente-io/museum/pkg/controller/public" "net/http" "strconv" @@ -10,7 +11,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/ente-io/museum/ente" - "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/museum/pkg/utils/handler" "github.com/ente-io/museum/pkg/utils/time" @@ -178,7 +178,7 @@ func (h *CollectionHandler) UpdateShareURL(c *gin.Context) { return } - if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > controller.DeviceLimitThreshold) { + if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > public.DeviceLimitThreshold) { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("device limit: %d out of range", *req.DeviceLimit))) return } diff --git a/server/pkg/api/file.go b/server/pkg/api/file.go index 1de2880de6..4ec205d1bb 100644 --- a/server/pkg/api/file.go +++ b/server/pkg/api/file.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/ente-io/museum/pkg/controller/file_copy" "github.com/ente-io/museum/pkg/controller/filedata" + "github.com/ente-io/museum/pkg/controller/public" "net/http" "os" "strconv" @@ -24,7 +25,7 @@ import ( // FileHandler exposes request handlers for all encrypted file related requests type FileHandler struct { Controller *controller.FileController - FileUrlCtrl *controller.PublicFileLinkController + FileUrlCtrl *public.FileLinkController FileCopyCtrl *file_copy.FileCopyController FileDataCtrl *filedata.Controller } diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go index 2e9da338dc..9073a0a331 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_url.go @@ -16,7 +16,7 @@ func (h *FileHandler) ShareUrl(c *gin.Context) { return } - response, err := h.FileUrlCtrl.CreateFileUrl(c, file) + response, err := h.FileUrlCtrl.CreateLink(c, file) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return diff --git a/server/pkg/api/public_collection.go b/server/pkg/api/public_collection.go index 9f61ba788e..81e1836f90 100644 --- a/server/pkg/api/public_collection.go +++ b/server/pkg/api/public_collection.go @@ -5,6 +5,7 @@ import ( fileData "github.com/ente-io/museum/ente/filedata" "github.com/ente-io/museum/pkg/controller/collections" "github.com/ente-io/museum/pkg/controller/filedata" + "github.com/ente-io/museum/pkg/controller/public" "net/http" "strconv" @@ -20,7 +21,7 @@ import ( // PublicCollectionHandler exposes request handlers for publicly accessible collections type PublicCollectionHandler struct { - Controller *controller.PublicCollectionController + Controller *public.CollectionLinkController FileCtrl *controller.FileController CollectionCtrl *collections.CollectionController FileDataCtrl *filedata.Controller diff --git a/server/pkg/controller/collections/collection.go b/server/pkg/controller/collections/collection.go index 5f096bc133..52a36df782 100644 --- a/server/pkg/controller/collections/collection.go +++ b/server/pkg/controller/collections/collection.go @@ -6,6 +6,7 @@ import ( "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/controller/access" "github.com/ente-io/museum/pkg/controller/email" + "github.com/ente-io/museum/pkg/controller/public" "github.com/ente-io/museum/pkg/repo/cast" "github.com/ente-io/museum/pkg/utils/array" "github.com/ente-io/museum/pkg/utils/auth" @@ -24,16 +25,16 @@ const ( // CollectionController encapsulates logic that deals with collections type CollectionController struct { - PublicCollectionCtrl *controller.PublicCollectionController - EmailCtrl *email.EmailNotificationController - AccessCtrl access.Controller - BillingCtrl *controller.BillingController - CollectionRepo *repo.CollectionRepository - UserRepo *repo.UserRepository - FileRepo *repo.FileRepository - QueueRepo *repo.QueueRepository - CastRepo *cast.Repository - TaskRepo *repo.TaskLockRepository + CollectionLinkController *public.CollectionLinkController + EmailCtrl *email.EmailNotificationController + AccessCtrl access.Controller + BillingCtrl *controller.BillingController + CollectionRepo *repo.CollectionRepository + UserRepo *repo.UserRepository + FileRepo *repo.FileRepository + QueueRepo *repo.QueueRepository + CastRepo *cast.Repository + TaskRepo *repo.TaskLockRepository } // Create creates a collection @@ -148,7 +149,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio } } - err = c.PublicCollectionCtrl.Disable(ctx, cID) + err = c.CollectionLinkController.Disable(ctx, cID) if err != nil { return stacktrace.Propagate(err, "failed to disabled public share url") } @@ -209,7 +210,7 @@ func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID if err != nil { return stacktrace.Propagate(err, "failed to revoke cast token for user") } - err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger) + err = c.CollectionLinkController.HandleAccountDeletion(ctx, userID, logger) return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/collections/share.go b/server/pkg/controller/collections/share.go index ced64f0fdf..7651266ece 100644 --- a/server/pkg/controller/collections/share.go +++ b/server/pkg/controller/collections/share.go @@ -70,21 +70,21 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec if !collection.AllowSharing() { return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type)) } - publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID) + collectionLinkToken, err := c.CollectionLinkController.GetActiveCollectionLinkToken(ctx, req.CollectionID) if err != nil { return stacktrace.Propagate(err, "") } - if canJoin := publicCollectionToken.CanJoin(); canJoin != nil { + if canJoin := collectionLinkToken.CanJoin(); canJoin != nil { return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error())) } accessToken := auth.GetAccessToken(ctx) - if publicCollectionToken.Token != accessToken { + if collectionLinkToken.Token != accessToken { return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection") } - if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" { + if collectionLinkToken.PassHash != nil && *collectionLinkToken.PassHash != "" { accessTokenJWT := auth.GetAccessTokenJWT(ctx) - if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil { + if passCheckErr := c.CollectionLinkController.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil { return stacktrace.Propagate(passCheckErr, "") } } @@ -93,7 +93,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec return stacktrace.Propagate(err, "") } role := ente.VIEWER - if publicCollectionToken.EnableCollect { + if collectionLinkToken.EnableCollect { role = ente.COLLABORATOR } joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds()) @@ -197,7 +197,7 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } - response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req) + response, err := c.CollectionLinkController.CreateLink(ctx, req) if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } @@ -214,7 +214,7 @@ func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } - response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req) + response, err := c.CollectionLinkController.UpdateSharedUrl(ctx, req) if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } @@ -226,7 +226,7 @@ func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int6 if err := c.verifyOwnership(cID, userID); err != nil { return stacktrace.Propagate(err, "") } - err := c.PublicCollectionCtrl.Disable(ctx, cID) + err := c.CollectionLinkController.Disable(ctx, cID) return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/public_collection.go b/server/pkg/controller/public/collection_link.go similarity index 86% rename from server/pkg/controller/public_collection.go rename to server/pkg/controller/public/collection_link.go index 7f51a46330..1c47b04cd9 100644 --- a/server/pkg/controller/public_collection.go +++ b/server/pkg/controller/public/collection_link.go @@ -1,9 +1,10 @@ -package controller +package public import ( "context" "errors" "fmt" + "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/repo/public" "github.com/ente-io/museum/ente" @@ -50,9 +51,9 @@ const ( AbuseLimitExceededTemplate = "report_limit_exceeded_alert.html" ) -// PublicCollectionController controls share collection operations -type PublicCollectionController struct { - FileController *FileController +// CollectionLinkController controls share collection operations +type CollectionLinkController struct { + FileController *controller.FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController PublicCollectionRepo *public.PublicCollectionRepository CollectionRepo *repo.CollectionRepository @@ -60,7 +61,7 @@ type PublicCollectionController struct { JwtSecret []byte } -func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) { +func (c *CollectionLinkController) CreateLink(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) @@ -92,11 +93,11 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req return response, nil } -func (c *PublicCollectionController) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) { +func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) { return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID) } -func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) { +func (c *CollectionLinkController) 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, "") @@ -119,12 +120,12 @@ func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File } // Disable all public accessTokens generated for the given cID till date. -func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error { +func (c *CollectionLinkController) 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) { +func (c *CollectionLinkController) 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 @@ -177,7 +178,7 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en // 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) { +func (c *CollectionLinkController) 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 { @@ -189,7 +190,7 @@ func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.V if req.PassHash != *publicCollectionToken.PassHash { return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link") } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{ + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.LinkPasswordClaim{ PassHash: req.PassHash, ExpiryTime: time.NDaysFromNow(365), }) @@ -204,8 +205,8 @@ func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.V }, 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) { +func (c *CollectionLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error { + token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, 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 } @@ -214,7 +215,7 @@ func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken if err != nil { return stacktrace.Propagate(err, "JWT parsed failed") } - claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim) + claims, ok := token.Claims.(*enteJWT.LinkPasswordClaim) if !ok { return stacktrace.Propagate(errors.New("no claim in jwt token"), "") @@ -228,7 +229,7 @@ func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken // ReportAbuse captures abuse report for a publicly shared collection. // It will also disable the accessToken for the collection if total abuse reports for the said collection // reaches AutoDisableAbuseThreshold -func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error { +func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error { accessContext := auth.MustGetPublicAccessContext(ctx) readableReason, found := AllowedReasons[req.Reason] if !found { @@ -254,7 +255,7 @@ func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.Abus return nil } -func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) { +func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) { collection, err := c.CollectionRepo.Get(collectionID) if err != nil { logrus.Error("Could not get collection for abuse report") @@ -293,7 +294,7 @@ func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, r } } -func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error { +func (c *CollectionLinkController) 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 { @@ -311,7 +312,7 @@ func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, // 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) { +func (c *CollectionLinkController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) { accessContext := auth.MustGetPublicAccessContext(ctx) collection, err := c.CollectionRepo.Get(accessContext.CollectionID) if err != nil { diff --git a/server/pkg/controller/public_file.go b/server/pkg/controller/public/file_link.go similarity index 64% rename from server/pkg/controller/public_file.go rename to server/pkg/controller/public/file_link.go index c74f1050df..bf1ad07865 100644 --- a/server/pkg/controller/public_file.go +++ b/server/pkg/controller/public/file_link.go @@ -1,8 +1,8 @@ -package controller +package public import ( "github.com/ente-io/museum/ente" - emailCtrl "github.com/ente-io/museum/pkg/controller/email" + "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/repo/public" "github.com/ente-io/museum/pkg/utils/auth" @@ -11,18 +11,16 @@ import ( "github.com/lithammer/shortuuid/v3" ) -// PublicFileLinkController controls share collection operations -type PublicFileLinkController struct { - FileController *FileController - EmailNotificationCtrl *emailCtrl.EmailNotificationController - PublicCollectionRepo *public.PublicCollectionRepository - FileLinkRepo *public.FileLinkRepository - CollectionRepo *repo.CollectionRepository - UserRepo *repo.UserRepository - JwtSecret []byte +// FileLinkController controls share collection operations +type FileLinkController struct { + FileController *controller.FileController + FileLinkRepo *public.FileLinkRepository + CollectionRepo *repo.CollectionRepository + UserRepo *repo.UserRepository + JwtSecret []byte } -func (c *PublicFileLinkController) CreateFileUrl(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { +func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { actorUserID := auth.GetUserID(ctx.Request.Header) accessToken := shortuuid.New()[0:AccessTokenLength] _, err := c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken) @@ -36,7 +34,7 @@ func (c *PublicFileLinkController) CreateFileUrl(ctx *gin.Context, req ente.Crea return nil, stacktrace.Propagate(err, "failed to create public file link") } -func (c *PublicFileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl { +func (c *FileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl { app := auth.GetApp(ctx) var url string if app == ente.Locker { diff --git a/server/pkg/controller/public/link_common.go b/server/pkg/controller/public/link_common.go new file mode 100644 index 0000000000..9fd24e5c1e --- /dev/null +++ b/server/pkg/controller/public/link_common.go @@ -0,0 +1 @@ +package public diff --git a/server/pkg/middleware/collection_token.go b/server/pkg/middleware/collection_token.go index 079f6f6f95..37ff02458e 100644 --- a/server/pkg/middleware/collection_token.go +++ b/server/pkg/middleware/collection_token.go @@ -5,6 +5,7 @@ import ( "context" "crypto/sha256" "fmt" + public2 "github.com/ente-io/museum/pkg/controller/public" "github.com/ente-io/museum/pkg/repo/public" "net/http" @@ -28,7 +29,7 @@ var whitelistedCollectionShareIDs = []int64{111} // CollectionTokenMiddleware intercepts and authenticates incoming requests type CollectionTokenMiddleware struct { PublicCollectionRepo *public.PublicCollectionRepository - PublicCollectionCtrl *controller.PublicCollectionController + PublicCollectionCtrl *public2.CollectionLinkController CollectionRepo *repo.CollectionRepository Cache *cache.Cache BillingCtrl *controller.BillingController @@ -143,11 +144,11 @@ func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, } deviceLimit := int64(collectionSummary.DeviceLimit) - if deviceLimit == controller.DeviceLimitThreshold { - deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold + if deviceLimit == public2.DeviceLimitThreshold { + deviceLimit = public2.DeviceLimitThresholdMultiplier * public2.DeviceLimitThreshold } - if count >= controller.DeviceLimitWarningThreshold { + if count >= public2.DeviceLimitWarningThreshold { if !array.Int64InList(sharedID, whitelistedCollectionShareIDs) { m.DiscordController.NotifyPotentialAbuse( fmt.Sprintf("Album exceeds warning threshold: {CollectionID: %d, ShareID: %d}", diff --git a/server/pkg/middleware/file_link_token.go b/server/pkg/middleware/file_link_token.go index 9ff26d2533..5c230882b5 100644 --- a/server/pkg/middleware/file_link_token.go +++ b/server/pkg/middleware/file_link_token.go @@ -3,6 +3,7 @@ package middleware import ( "context" "fmt" + publicCtrl "github.com/ente-io/museum/pkg/controller/public" "github.com/ente-io/museum/pkg/repo/public" "net/http" @@ -25,7 +26,7 @@ var filePasswordWhiteListedURLs = []string{"/public-collection/info", "/public-c // FileLinkMiddleware intercepts and authenticates incoming requests type FileLinkMiddleware struct { FileLinkRepo *public.FileLinkRepository - PublicCollectionCtrl *controller.PublicCollectionController + PublicCollectionCtrl *publicCtrl.CollectionLinkController CollectionRepo *repo.CollectionRepository Cache *cache.Cache BillingCtrl *controller.BillingController @@ -140,13 +141,13 @@ func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context, } deviceLimit := int64(collectionSummary.DeviceLimit) - if deviceLimit == controller.DeviceLimitThreshold { - deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold + if deviceLimit == publicCtrl.DeviceLimitThreshold { + deviceLimit = publicCtrl.DeviceLimitThresholdMultiplier * publicCtrl.DeviceLimitThreshold } - if count >= controller.DeviceLimitWarningThreshold { + if count >= publicCtrl.DeviceLimitWarningThreshold { m.DiscordController.NotifyPotentialAbuse( - fmt.Sprintf("Album exceeds warning threshold: {FileID: %d, ShareID: %s}", + fmt.Sprintf("FileLink exceeds warning threshold: {FileID: %d, ShareID: %s}", collectionSummary.FileID, collectionSummary.LinkID)) } diff --git a/server/pkg/repo/public/public_collection.go b/server/pkg/repo/public/public_collection.go index 600c213c11..077d1889e8 100644 --- a/server/pkg/repo/public/public_collection.go +++ b/server/pkg/repo/public/public_collection.go @@ -92,26 +92,26 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con return result, nil } -// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID +// GetActivePublicCollectionToken will return ente.CollectionLinkRow for given collection ID // Note: The token could be expired or deviceLimit is already reached -func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) { +func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, 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{} + ret := ente.CollectionLinkRow{} 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 ente.CollectionLinkRow{}, stacktrace.Propagate(err, "") } return ret, nil } // UpdatePublicCollectionToken will update the row for corresponding public collection token -func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.PublicCollectionToken) error { +func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) 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`, diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/public_file.go index 022a6ba7b0..20573031a7 100644 --- a/server/pkg/repo/public/public_file.go +++ b/server/pkg/repo/public/public_file.go @@ -63,7 +63,7 @@ func (pcr *FileLinkRepository) Insert( return id, nil } -// GetActiveFileUrlToken will return ente.PublicCollectionToken for given collection ID +// GetActiveFileUrlToken will return ente.CollectionLinkRow for given collection ID // Note: The token could be expired or deviceLimit is already reached func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit, From 51c00eefd410f5fc283bcdc6f556953ec6241ffd Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:27:21 +0530 Subject: [PATCH 10/57] Support for link password validation --- server/ente/public_file.go | 4 +- .../pkg/controller/public/collection_link.go | 47 ++-------------- server/pkg/controller/public/file_link.go | 17 ++++++ server/pkg/controller/public/link_common.go | 53 +++++++++++++++++++ server/pkg/middleware/file_link_token.go | 42 +++++++-------- server/pkg/repo/public/public_collection.go | 4 +- server/pkg/utils/auth/auth.go | 4 ++ 7 files changed, 104 insertions(+), 67 deletions(-) diff --git a/server/ente/public_file.go b/server/ente/public_file.go index 3b43ac0f43..12398c38a1 100644 --- a/server/ente/public_file.go +++ b/server/ente/public_file.go @@ -51,8 +51,8 @@ type FileUrl struct { CreatedAt int64 `json:"createdAt"` } -type PublicFileAccessContext struct { - ID string +type FileLinkAccessContext struct { + LinkID string IP string UserAgent string FileID int64 diff --git a/server/pkg/controller/public/collection_link.go b/server/pkg/controller/public/collection_link.go index 1c47b04cd9..bec8393425 100644 --- a/server/pkg/controller/public/collection_link.go +++ b/server/pkg/controller/public/collection_link.go @@ -8,7 +8,6 @@ import ( "github.com/ente-io/museum/pkg/repo/public" "github.com/ente-io/museum/ente" - enteJWT "github.com/ente-io/museum/ente/jwt" emailCtrl "github.com/ente-io/museum/pkg/controller/email" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/utils/auth" @@ -16,7 +15,6 @@ import ( "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" ) @@ -94,7 +92,7 @@ func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.Crea } func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) { - return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID) + return c.PublicCollectionRepo.GetActiveCollectionLinkRow(ctx, collectionID) } func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) { @@ -126,7 +124,7 @@ func (c *CollectionLinkController) Disable(ctx context.Context, cID int64) error } func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) { - publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID) + publicCollectionToken, err := c.PublicCollectionRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID) if err != nil { return ente.PublicURL{}, err } @@ -180,50 +178,15 @@ func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente // attack for guessing password. func (c *CollectionLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) { accessContext := auth.MustGetPublicAccessContext(ctx) - publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID) + collectionLinkRow, err := c.PublicCollectionRepo.GetActiveCollectionLinkRow(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.LinkPasswordClaim{ - 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 + return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req) } func (c *CollectionLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error { - token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, 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.LinkPasswordClaim) - - if !ok { - return stacktrace.Propagate(errors.New("no claim in jwt token"), "") - } - if token.Valid && claims.PassHash == passwordHash { - return nil - } - return ente.ErrInvalidPassword + return validateJWTToken(c.JwtSecret, jwtToken, passwordHash) } // ReportAbuse captures abuse report for a publicly shared collection. diff --git a/server/pkg/controller/public/file_link.go b/server/pkg/controller/public/file_link.go index bf1ad07865..edb90ec155 100644 --- a/server/pkg/controller/public/file_link.go +++ b/server/pkg/controller/public/file_link.go @@ -34,6 +34,23 @@ func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl return nil, stacktrace.Propagate(err, "failed to create public file link") } +// 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 *FileLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) { + accessContext := auth.MustGetFileLinkAccessContext(ctx) + collectionLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, accessContext.FileID) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get public collection info") + } + return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req) +} + +func (c *FileLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error { + return validateJWTToken(c.JwtSecret, jwtToken, passwordHash) +} + func (c *FileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl { app := auth.GetApp(ctx) var url string diff --git a/server/pkg/controller/public/link_common.go b/server/pkg/controller/public/link_common.go index 9fd24e5c1e..9a56334a0e 100644 --- a/server/pkg/controller/public/link_common.go +++ b/server/pkg/controller/public/link_common.go @@ -1 +1,54 @@ package public + +import ( + "errors" + "fmt" + "github.com/ente-io/museum/ente" + enteJWT "github.com/ente-io/museum/ente/jwt" + "github.com/ente-io/museum/pkg/utils/time" + "github.com/ente-io/stacktrace" + "github.com/golang-jwt/jwt" +) + +func validateJWTToken(secret []byte, jwtToken string, passwordHash string) error { + token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, 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 secret, nil + }) + if err != nil { + return stacktrace.Propagate(err, "JWT parsed failed") + } + claims, ok := token.Claims.(*enteJWT.LinkPasswordClaim) + + if !ok { + return stacktrace.Propagate(errors.New("no claim in jwt token"), "") + } + if token.Valid && claims.PassHash == passwordHash { + return nil + } + return ente.ErrInvalidPassword +} + +func verifyPassword(secret []byte, expectedPassHash *string, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) { + if expectedPassHash == nil || *expectedPassHash == "" { + return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link") + } + if req.PassHash != *expectedPassHash { + return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link") + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.LinkPasswordClaim{ + PassHash: req.PassHash, + ExpiryTime: time.NDaysFromNow(365), + }) + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(secret) + + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return &ente.VerifyPasswordResponse{ + JWTToken: tokenString, + }, nil +} diff --git a/server/pkg/middleware/file_link_token.go b/server/pkg/middleware/file_link_token.go index 5c230882b5..539ca1179e 100644 --- a/server/pkg/middleware/file_link_token.go +++ b/server/pkg/middleware/file_link_token.go @@ -25,12 +25,12 @@ var filePasswordWhiteListedURLs = []string{"/public-collection/info", "/public-c // FileLinkMiddleware intercepts and authenticates incoming requests type FileLinkMiddleware struct { - FileLinkRepo *public.FileLinkRepository - PublicCollectionCtrl *publicCtrl.CollectionLinkController - CollectionRepo *repo.CollectionRepository - Cache *cache.Cache - BillingCtrl *controller.BillingController - DiscordController *discord.DiscordController + FileLinkRepo *public.FileLinkRepository + FileLinkCtrl *publicCtrl.FileLinkController + CollectionRepo *repo.CollectionRepository + Cache *cache.Cache + BillingCtrl *controller.BillingController + DiscordController *discord.DiscordController } // Authenticate returns a middle ware that extracts the `X-Auth-Access-Token` @@ -48,27 +48,27 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":") cachedValue, cacheHit := m.Cache.Get(cacheKey) - var publicCollectionSummary *ente.FileLinkRow + var fileLinkRow *ente.FileLinkRow var err error if !cacheHit { - publicCollectionSummary, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken) + fileLinkRow, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } - if publicCollectionSummary.IsDisabled { + if fileLinkRow.IsDisabled { c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "disabled token"}) return } // validate if user still has active paid subscription - if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(publicCollectionSummary.OwnerID, true); err != nil { + if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(fileLinkRow.OwnerID, true); err != nil { logrus.WithError(err).Warn("failed to verify active paid subscription") c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"}) return } // validate device limit - reached, err := m.isDeviceLimitReached(c, publicCollectionSummary, clientIP, userAgent) + reached, err := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent) if err != nil { logrus.WithError(err).Error("failed to check device limit") c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"}) @@ -79,19 +79,19 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri return } } else { - publicCollectionSummary = cachedValue.(*ente.FileLinkRow) + fileLinkRow = cachedValue.(*ente.FileLinkRow) } - if publicCollectionSummary.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry - publicCollectionSummary.ValidTill < time.Microseconds() { + if fileLinkRow.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry + fileLinkRow.ValidTill < time.Microseconds() { c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "expired token"}) return } // checks password protected public collection - if publicCollectionSummary.PassHash != nil && *publicCollectionSummary.PassHash != "" { + if fileLinkRow.PassHash != nil && *fileLinkRow.PassHash != "" { reqPath := urlSanitizer(c) - if err = m.validatePassword(c, reqPath, publicCollectionSummary); err != nil { + if err = m.validatePassword(c, reqPath, fileLinkRow); err != nil { logrus.WithError(err).Warn("password validation failed") c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) return @@ -99,14 +99,14 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri } if !cacheHit { - m.Cache.Set(cacheKey, publicCollectionSummary, cache.DefaultExpiration) + m.Cache.Set(cacheKey, fileLinkRow, cache.DefaultExpiration) } - c.Set(auth.FileLinkAccessKey, &ente.PublicFileAccessContext{ - ID: publicCollectionSummary.LinkID, + c.Set(auth.FileLinkAccessKey, &ente.FileLinkAccessContext{ + LinkID: fileLinkRow.LinkID, IP: clientIP, UserAgent: userAgent, - FileID: publicCollectionSummary.FileID, + FileID: fileLinkRow.FileID, }) c.Next() } @@ -168,5 +168,5 @@ func (m *FileLinkMiddleware) validatePassword(c *gin.Context, reqPath string, if accessTokenJWT == "" { return ente.ErrAuthenticationRequired } - return m.PublicCollectionCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash) + return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash) } diff --git a/server/pkg/repo/public/public_collection.go b/server/pkg/repo/public/public_collection.go index 077d1889e8..5a73c2e43f 100644 --- a/server/pkg/repo/public/public_collection.go +++ b/server/pkg/repo/public/public_collection.go @@ -92,9 +92,9 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con return result, nil } -// GetActivePublicCollectionToken will return ente.CollectionLinkRow for given collection ID +// GetActiveCollectionLinkRow will return ente.CollectionLinkRow for given collection ID // Note: The token could be expired or deviceLimit is already reached -func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) { +func (pcr *PublicCollectionRepository) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, 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`, diff --git a/server/pkg/utils/auth/auth.go b/server/pkg/utils/auth/auth.go index e352d168f1..85acc995c3 100644 --- a/server/pkg/utils/auth/auth.go +++ b/server/pkg/utils/auth/auth.go @@ -133,6 +133,10 @@ func MustGetPublicAccessContext(c *gin.Context) ente.PublicAccessContext { return c.MustGet(PublicAccessKey).(ente.PublicAccessContext) } +func MustGetFileLinkAccessContext(c *gin.Context) *ente.FileLinkAccessContext { + return c.MustGet(FileLinkAccessKey).(*ente.FileLinkAccessContext) +} + func GetCastCtx(c *gin.Context) cast.AuthContext { return c.MustGet(CastContext).(cast.AuthContext) } From 13420e44409d1cc7b68fb2d8c315e93aa384e667 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:04:21 +0530 Subject: [PATCH 11/57] Endpoints for create,edit,delete and fetch links --- server/cmd/museum/main.go | 23 ++--- server/ente/{public_file.go => file_link.go} | 34 ++++++++ server/ente/public_collection.go | 29 ++++++- server/migrations/102_single_file_url.up.sql | 3 +- server/pkg/api/collection.go | 30 ------- server/pkg/api/file_url.go | 56 ++++++++++++ .../pkg/controller/collections/collection.go | 24 +++--- server/pkg/controller/collections/share.go | 28 +++--- server/pkg/controller/public/file_link.go | 86 +++++++++++++++++-- server/pkg/repo/file.go | 10 +++ server/pkg/repo/public/public_file.go | 72 +++++++++++++++- server/pkg/utils/auth/auth.go | 2 + 12 files changed, 324 insertions(+), 73 deletions(-) rename server/ente/{public_file.go => file_link.go} (57%) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index ed731b51da..21519ca285 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -311,16 +311,16 @@ func main() { } collectionController := &collections.CollectionController{ - CollectionRepo: collectionRepo, - EmailCtrl: emailNotificationCtrl, - AccessCtrl: accessCtrl, - CollectionLinkController: collectionLinkCtrl, - UserRepo: userRepo, - FileRepo: fileRepo, - CastRepo: &castDb, - BillingCtrl: billingController, - QueueRepo: queueRepo, - TaskRepo: taskLockingRepo, + CollectionRepo: collectionRepo, + EmailCtrl: emailNotificationCtrl, + AccessCtrl: accessCtrl, + CollectionLinkCtrl: collectionLinkCtrl, + UserRepo: userRepo, + FileRepo: fileRepo, + CastRepo: &castDb, + BillingCtrl: billingController, + QueueRepo: queueRepo, + TaskRepo: taskLockingRepo, } kexCtrl := &kexCtrl.Controller{ @@ -442,6 +442,9 @@ func main() { privateAPI.GET("/files/preview/v2/:fileID", fileHandler.GetThumbnail) privateAPI.POST("files/share-url", fileHandler.ShareUrl) + privateAPI.PUT("files/share-url", fileHandler.UpdateFileURL) + privateAPI.DELETE("files/share-url/:fileID", fileHandler.DisableUrl) + privateAPI.GET("files/share-urls/", fileHandler.DisableUrl) privateAPI.PUT("/files/data", fileHandler.PutFileData) privateAPI.PUT("/files/video-data", fileHandler.PutVideoData) diff --git a/server/ente/public_file.go b/server/ente/file_link.go similarity index 57% rename from server/ente/public_file.go rename to server/ente/file_link.go index 12398c38a1..a69034bf11 100644 --- a/server/ente/public_file.go +++ b/server/ente/file_link.go @@ -1,8 +1,14 @@ package ente +import ( + "fmt" + "github.com/ente-io/museum/pkg/utils/time" +) + // CreateFileUrl represents an encrypted file in the system type CreateFileUrl struct { FileID int64 `json:"fileID" binding:"required"` + App App `json:"app" binding:"required"` } // UpdateFileUrl .. @@ -19,6 +25,33 @@ type UpdateFileUrl struct { DisablePassword *bool `json:"disablePassword"` } +func (ut *UpdateFileUrl) Validate() error { + if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil && + ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil { + return NewBadRequestWithMessage("all parameters are missing") + } + + if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) { + return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit)) + } + + if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() { + return NewBadRequestWithMessage("valid till should be greater than current timestamp") + } + + var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil + var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil + + if !(allPassParamsMissing || allPassParamsPresent) { + return NewBadRequestWithMessage("all password params should be either present or missing") + } + + if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword { + return NewBadRequestWithMessage("can not set and disable password in same request") + } + return nil +} + type FileLinkRow struct { LinkID string OwnerID int64 @@ -35,6 +68,7 @@ type FileLinkRow struct { CreatedAt int64 UpdatedAt int64 } + type FileUrl struct { LinkID string `json:"linkID" binding:"required"` URL string `json:"url" binding:"required"` diff --git a/server/ente/public_collection.go b/server/ente/public_collection.go index f34c0bf2f1..5f3867e6d0 100644 --- a/server/ente/public_collection.go +++ b/server/ente/public_collection.go @@ -3,7 +3,7 @@ package ente import ( "database/sql/driver" "encoding/json" - + "fmt" "github.com/ente-io/museum/pkg/utils/time" "github.com/ente-io/stacktrace" ) @@ -32,6 +32,33 @@ type UpdatePublicAccessTokenRequest struct { EnableJoin *bool `json:"enableJoin"` } +func (ut *UpdatePublicAccessTokenRequest) Validate() error { + if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil && + ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil { + return NewBadRequestWithMessage("all parameters are missing") + } + + if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) { + return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit)) + } + + if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() { + return NewBadRequestWithMessage("valid till should be greater than current timestamp") + } + + var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil + var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil + + if !(allPassParamsMissing || allPassParamsPresent) { + return NewBadRequestWithMessage("all password params should be either present or missing") + } + + if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword { + return NewBadRequestWithMessage("can not set and disable password in same request") + } + return nil +} + type VerifyPasswordRequest struct { PassHash string `json:"passHash" binding:"required"` } diff --git a/server/migrations/102_single_file_url.up.sql b/server/migrations/102_single_file_url.up.sql index 5a1f55089a..0f94897ba2 100644 --- a/server/migrations/102_single_file_url.up.sql +++ b/server/migrations/102_single_file_url.up.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS public_file_tokens id text primary key, file_id bigint NOT NULL, owner_id bigint NOT NULL, + app text NOT NULL, access_token text not null, valid_till bigint not null DEFAULT 0, device_limit int not null DEFAULT 0, @@ -40,6 +41,6 @@ CREATE TABLE IF NOT EXISTS public_file_tokens_access_history ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS public_access_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE; +CREATE UNIQUE INDEX IF NOT EXISTS public_file_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE; CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at); diff --git a/server/pkg/api/collection.go b/server/pkg/api/collection.go index d5f918c672..6c198652c7 100644 --- a/server/pkg/api/collection.go +++ b/server/pkg/api/collection.go @@ -3,7 +3,6 @@ package api import ( "fmt" "github.com/ente-io/museum/pkg/controller/collections" - "github.com/ente-io/museum/pkg/controller/public" "net/http" "strconv" @@ -172,35 +171,6 @@ func (h *CollectionHandler) UpdateShareURL(c *gin.Context) { handler.Error(c, stacktrace.Propagate(err, "")) return } - if req.DeviceLimit == nil && req.ValidTill == nil && req.DisablePassword == nil && - req.Nonce == nil && req.PassHash == nil && req.EnableDownload == nil && req.EnableCollect == nil { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all parameters are missing")) - return - } - - if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > public.DeviceLimitThreshold) { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("device limit: %d out of range", *req.DeviceLimit))) - return - } - - if req.ValidTill != nil && *req.ValidTill != 0 && *req.ValidTill < time.Microseconds() { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "valid till should be greater than current timestamp")) - return - } - - var allPassParamsMissing = req.Nonce == nil && req.PassHash == nil && req.MemLimit == nil && req.OpsLimit == nil - var allPassParamsPresent = req.Nonce != nil && req.PassHash != nil && req.MemLimit != nil && req.OpsLimit != nil - - if !(allPassParamsMissing || allPassParamsPresent) { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all password params should be either present or missing")) - return - } - - if allPassParamsPresent && req.DisablePassword != nil && *req.DisablePassword { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "can not set and disable password in same request")) - return - } - response, err := h.Controller.UpdateShareURL(c, auth.GetUserID(c.Request.Header), req) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go index 9073a0a331..1780345bfc 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_url.go @@ -6,6 +6,7 @@ import ( "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" "net/http" + "strconv" ) // ShareUrl a sharable url for the file @@ -23,3 +24,58 @@ func (h *FileHandler) ShareUrl(c *gin.Context) { } c.JSON(http.StatusOK, response) } + +func (h *FileHandler) DisableUrl(c *gin.Context) { + cID, err := strconv.ParseInt(c.Param("fileID"), 10, 64) + if err != nil { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "")) + return + } + err = h.FileUrlCtrl.Disable(c, cID) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{}) +} + +func (h *FileHandler) GetUrls(c *gin.Context) { + sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64) + if err != nil { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "")) + return + } + limit := 500 + if c.Query("limit") != "" { + limit, err = strconv.Atoi(c.Query("limit")) + if err != nil || limit < 1 { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "")) + return + } + } + response, err := h.FileUrlCtrl.GetUrls(c, sinceTime, int64(limit)) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{ + "diff": response, + }) +} + +// UpdateFileURL updates the share URL for a file +func (h *FileHandler) UpdateFileURL(c *gin.Context) { + var req ente.UpdateFileUrl + if err := c.ShouldBindJSON(&req); err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + response, err := h.FileUrlCtrl.UpdateSharedUrl(c, req) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{ + "result": response, + }) +} diff --git a/server/pkg/controller/collections/collection.go b/server/pkg/controller/collections/collection.go index 52a36df782..5b86d38d0a 100644 --- a/server/pkg/controller/collections/collection.go +++ b/server/pkg/controller/collections/collection.go @@ -25,16 +25,16 @@ const ( // CollectionController encapsulates logic that deals with collections type CollectionController struct { - CollectionLinkController *public.CollectionLinkController - EmailCtrl *email.EmailNotificationController - AccessCtrl access.Controller - BillingCtrl *controller.BillingController - CollectionRepo *repo.CollectionRepository - UserRepo *repo.UserRepository - FileRepo *repo.FileRepository - QueueRepo *repo.QueueRepository - CastRepo *cast.Repository - TaskRepo *repo.TaskLockRepository + CollectionLinkCtrl *public.CollectionLinkController + EmailCtrl *email.EmailNotificationController + AccessCtrl access.Controller + BillingCtrl *controller.BillingController + CollectionRepo *repo.CollectionRepository + UserRepo *repo.UserRepository + FileRepo *repo.FileRepository + QueueRepo *repo.QueueRepository + CastRepo *cast.Repository + TaskRepo *repo.TaskLockRepository } // Create creates a collection @@ -149,7 +149,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio } } - err = c.CollectionLinkController.Disable(ctx, cID) + err = c.CollectionLinkCtrl.Disable(ctx, cID) if err != nil { return stacktrace.Propagate(err, "failed to disabled public share url") } @@ -210,7 +210,7 @@ func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID if err != nil { return stacktrace.Propagate(err, "failed to revoke cast token for user") } - err = c.CollectionLinkController.HandleAccountDeletion(ctx, userID, logger) + err = c.CollectionLinkCtrl.HandleAccountDeletion(ctx, userID, logger) return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/collections/share.go b/server/pkg/controller/collections/share.go index 7651266ece..6002c7b493 100644 --- a/server/pkg/controller/collections/share.go +++ b/server/pkg/controller/collections/share.go @@ -70,7 +70,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec if !collection.AllowSharing() { return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type)) } - collectionLinkToken, err := c.CollectionLinkController.GetActiveCollectionLinkToken(ctx, req.CollectionID) + collectionLinkToken, err := c.CollectionLinkCtrl.GetActiveCollectionLinkToken(ctx, req.CollectionID) if err != nil { return stacktrace.Propagate(err, "") } @@ -84,7 +84,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec } if collectionLinkToken.PassHash != nil && *collectionLinkToken.PassHash != "" { accessTokenJWT := auth.GetAccessTokenJWT(ctx) - if passCheckErr := c.CollectionLinkController.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil { + if passCheckErr := c.CollectionLinkCtrl.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil { return stacktrace.Propagate(passCheckErr, "") } } @@ -197,7 +197,7 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } - response, err := c.CollectionLinkController.CreateLink(ctx, req) + response, err := c.CollectionLinkCtrl.CreateLink(ctx, req) if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } @@ -205,20 +205,26 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e } // UpdateShareURL updates the shared url configuration -func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) ( - ente.PublicURL, error) { +func (c *CollectionController) UpdateShareURL( + ctx context.Context, + userID int64, + req ente.UpdatePublicAccessTokenRequest, +) (*ente.PublicURL, error) { + if err := req.Validate(); err != nil { + return nil, stacktrace.Propagate(err, "") + } if err := c.verifyOwnership(req.CollectionID, userID); err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } - response, err := c.CollectionLinkController.UpdateSharedUrl(ctx, req) + response, err := c.CollectionLinkCtrl.UpdateSharedUrl(ctx, req) if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } - return response, nil + return &response, nil } // DisableSharedURL disable a public auth-token for the given collectionID @@ -226,7 +232,7 @@ func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int6 if err := c.verifyOwnership(cID, userID); err != nil { return stacktrace.Propagate(err, "") } - err := c.CollectionLinkController.Disable(ctx, cID) + err := c.CollectionLinkCtrl.Disable(ctx, cID) return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/public/file_link.go b/server/pkg/controller/public/file_link.go index edb90ec155..39f1ba9fe7 100644 --- a/server/pkg/controller/public/file_link.go +++ b/server/pkg/controller/public/file_link.go @@ -15,17 +15,27 @@ import ( type FileLinkController struct { FileController *controller.FileController FileLinkRepo *public.FileLinkRepository - CollectionRepo *repo.CollectionRepository - UserRepo *repo.UserRepository + FileRepo *repo.FileRepository JwtSecret []byte } func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) { actorUserID := auth.GetUserID(ctx.Request.Header) + app := auth.GetApp(ctx) + if req.App != app { + return nil, stacktrace.Propagate(ente.NewBadRequestWithMessage("app mismatch"), "app mismatch") + } + file, err := c.FileRepo.GetFileAttributes(req.FileID) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get file attributes") + } + if actorUserID != file.OwnerID { + return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "") + } accessToken := shortuuid.New()[0:AccessTokenLength] - _, err := c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken) - if err == nil { - row, rowErr := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID) + _, err = c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken, app) + if err == nil || err == ente.ErrActiveLinkAlreadyExists { + row, rowErr := c.FileLinkRepo.GetFileUrlRowByFileID(ctx, req.FileID) if rowErr != nil { return nil, stacktrace.Propagate(rowErr, "failed to get active file url token") } @@ -34,6 +44,72 @@ func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl return nil, stacktrace.Propagate(err, "failed to create public file link") } +// Disable all public accessTokens generated for the given fileID till date. +func (c *FileLinkController) Disable(ctx *gin.Context, fileID int64) error { + userID := auth.GetUserID(ctx.Request.Header) + file, err := c.FileRepo.GetFileAttributes(fileID) + if err != nil { + return stacktrace.Propagate(err, "failed to get file attributes") + } + if userID != file.OwnerID { + return stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "") + } + return c.FileLinkRepo.DisableLinkForFiles(ctx, []int64{fileID}) +} + +func (c *FileLinkController) GetUrls(ctx *gin.Context, sinceTime int64, limit int64) ([]*ente.FileUrl, error) { + userID := auth.GetUserID(ctx.Request.Header) + app := auth.GetApp(ctx) + fileLinks, err := c.FileLinkRepo.GetFileUrls(ctx, userID, sinceTime, limit, app) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get file urls") + } + var fileUrls []*ente.FileUrl + for _, row := range fileLinks { + fileUrls = append(fileUrls, c.mapRowToFileUrl(ctx, row)) + } + return fileUrls, nil +} + +func (c *FileLinkController) UpdateSharedUrl(ctx *gin.Context, req ente.UpdateFileUrl) (*ente.FileUrl, error) { + if err := req.Validate(); err != nil { + return nil, stacktrace.Propagate(err, "invalid request") + } + fileLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get file link info") + } + if fileLinkRow.OwnerID != auth.GetUserID(ctx.Request.Header) { + return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "") + } + if req.ValidTill != nil { + fileLinkRow.ValidTill = *req.ValidTill + } + if req.DeviceLimit != nil { + fileLinkRow.DeviceLimit = *req.DeviceLimit + } + if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil { + fileLinkRow.PassHash = req.PassHash + fileLinkRow.Nonce = req.Nonce + fileLinkRow.OpsLimit = req.OpsLimit + fileLinkRow.MemLimit = req.MemLimit + } else if req.DisablePassword != nil && *req.DisablePassword { + fileLinkRow.PassHash = nil + fileLinkRow.Nonce = nil + fileLinkRow.OpsLimit = nil + fileLinkRow.MemLimit = nil + } + if req.EnableDownload != nil { + fileLinkRow.EnableDownload = *req.EnableDownload + } + + err = c.FileLinkRepo.UpdateLink(ctx, *fileLinkRow) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return c.mapRowToFileUrl(ctx, fileLinkRow), 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 diff --git a/server/pkg/repo/file.go b/server/pkg/repo/file.go index 2ae4eafdca..50945cf6b1 100644 --- a/server/pkg/repo/file.go +++ b/server/pkg/repo/file.go @@ -638,6 +638,16 @@ func (repo *FileRepository) GetFileAttributesForCopy(fileIDs []int64) ([]ente.Fi return result, nil } +func (repo *FileRepository) GetFileAttributes(fileID int64) (*ente.File, error) { + rows := repo.DB.QueryRow(`SELECT file_id, owner_id, file_decryption_header, thumbnail_decryption_header, metadata_decryption_header, encrypted_metadata, pub_magic_metadata FROM files WHERE file_id = $1`, fileID) + var file ente.File + err := rows.Scan(&file.ID, &file.OwnerID, &file.File.DecryptionHeader, &file.Thumbnail.DecryptionHeader, &file.Metadata.DecryptionHeader, &file.Metadata.EncryptedData, &file.PubicMagicMetadata) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return &file, nil +} + // GetUsage gets the Storage usage of a user // Deprecated: GetUsage is deprecated, use UsageRepository.GetUsage func (repo *FileRepository) GetUsage(userID int64) (int64, error) { diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/public_file.go index 20573031a7..3f6dbf7b33 100644 --- a/server/pkg/repo/public/public_file.go +++ b/server/pkg/repo/public/public_file.go @@ -46,16 +46,17 @@ func (pcr *FileLinkRepository) Insert( fileID int64, ownerID int64, token string, + app ente.App, ) (*string, error) { id, err := base.NewID("pft") if err != nil { return nil, 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) + (id, file_id, owner_id, access_token, app) VALUES ($1, $2, $3, $4, $5)`, + id, fileID, ownerID, token, string(app)) if err != nil { - if err.Error() == "pq: duplicate key value violates unique constraint \"public_access_token_unique_idx\"" { + if err.Error() == "pq: duplicate key value violates unique constraint \"public_file_token_unique_idx\"" { return nil, ente.ErrActiveLinkAlreadyExists } return nil, stacktrace.Propagate(err, "failed to insert") @@ -79,6 +80,54 @@ func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID } return &ret, nil } +func (pcr *FileLinkRepository) GetFileUrls(ctx context.Context, userID int64, sinceTime int64, limit int64, app ente.App) ([]*ente.FileLinkRow, error) { + if limit <= 0 { + limit = 500 + } + query := `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit, + created_at, updated_at FROM public_file_tokens + WHERE owner_id = $1 AND created_at > $2 AND app = $3 ORDER BY updated_at DESC LIMIT $4` + rows, err := pcr.DB.QueryContext(ctx, query, userID, sinceTime, string(app), limit) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to get public file urls") + } + defer rows.Close() + + var result []*ente.FileLinkRow + for rows.Next() { + var row ente.FileLinkRow + err = rows.Scan(&row.LinkID, &row.FileID, &row.OwnerID, &row.IsDisabled, + &row.ValidTill, &row.DeviceLimit, &row.EnableDownload, + &row.PassHash, &row.Nonce, &row.MemLimit, + &row.OpsLimit, &row.CreatedAt, &row.UpdatedAt) + if err != nil { + return nil, stacktrace.Propagate(err, "failed to scan public file url row") + } + result = append(result, &row) + } + return result, nil +} + +func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs []int64) error { + if len(fileIDs) == 0 { + return nil + } + query := `UPDATE public_file_tokens SET is_disabled = TRUE WHERE file_id = ANY($1)` + _, err := pcr.DB.ExecContext(ctx, query, fileIDs) + if err != nil { + return stacktrace.Propagate(err, "failed to disable public file links") + } + return nil +} + +// DisableLinksForUser will disable all public file links for the given user +func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, linkID string) error { + _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE id = $1`, linkID) + if err != nil { + return stacktrace.Propagate(err, "failed to disable public file link") + } + return nil +} func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, @@ -98,6 +147,23 @@ func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessT return &result, nil } +func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) { + row := pcr.DB.QueryRowContext(ctx, + `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit + created_at, updated_at + from public_file_tokens + where file_id = $1 and is_disabled = FALSE`, fileID) + var result = ente.FileLinkRow{} + err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &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 file ID") + } + return &result, nil +} + // UpdateLink will update the row for corresponding public file token func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.FileLinkRow) error { _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2, diff --git a/server/pkg/utils/auth/auth.go b/server/pkg/utils/auth/auth.go index 85acc995c3..8b52808e36 100644 --- a/server/pkg/utils/auth/auth.go +++ b/server/pkg/utils/auth/auth.go @@ -121,6 +121,8 @@ func GetCastToken(c *gin.Context) string { return token } +// GetAccessTokenJWT fetches the JWT access token from the request header or query parameters. +// This token is issued by server on password verification of links that are protected by password. func GetAccessTokenJWT(c *gin.Context) string { token := c.GetHeader("X-Auth-Access-Token-JWT") if token == "" { From 944bdfc7fa7921bd638aa1de2e5082322ee0c544 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:09:12 +0530 Subject: [PATCH 12/57] Rename --- server/cmd/museum/main.go | 21 ++++++---- .../pkg/controller/public/collection_link.go | 26 ++++++------- server/pkg/middleware/collection_token.go | 10 ++--- server/pkg/repo/collection.go | 18 ++++----- ...ublic_collection.go => collection_link.go} | 38 +++++++++---------- .../public/{public_file.go => file_link.go} | 0 6 files changed, 60 insertions(+), 53 deletions(-) rename server/pkg/repo/public/{public_collection.go => collection_link.go} (80%) rename server/pkg/repo/public/{public_file.go => file_link.go} (100%) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 21519ca285..bda540113e 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -178,8 +178,8 @@ func main() { fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo} familyRepo := &repo.FamilyRepository{DB: db} trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo} - publicCollectionRepo := public.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums")) - collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo, + collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums")) + collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo, TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger} pushRepo := &repo.PushTokenRepository{DB: db} kexRepo := &kex.Repository{ @@ -304,7 +304,7 @@ func main() { collectionLinkCtrl := &publicCtrl.CollectionLinkController{ FileController: fileController, EmailNotificationCtrl: emailNotificationCtrl, - PublicCollectionRepo: publicCollectionRepo, + CollectionLinkRepo: collectionLinkRepo, CollectionRepo: collectionRepo, UserRepo: userRepo, JwtSecret: jwtSecretBytes, @@ -360,7 +360,7 @@ func main() { authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController} collectionTokenMiddleware := middleware.CollectionTokenMiddleware{ - PublicCollectionRepo: publicCollectionRepo, + CollectionLinkRepo: collectionLinkRepo, PublicCollectionCtrl: collectionLinkCtrl, CollectionRepo: collectionRepo, Cache: accessTokenCache, @@ -428,11 +428,18 @@ func main() { ObjectRepo: objectRepo, FileRepo: fileRepo, } + fileLinkCtrl := &publicCtrl.FileLinkController{ + FileController: fileController, + FileLinkRepo: nil, + FileRepo: fileRepo, + JwtSecret: jwtSecretBytes, + } fileHandler := &api.FileHandler{ Controller: fileController, FileCopyCtrl: fileCopyCtrl, FileDataCtrl: fileDataCtrl, + FileUrlCtrl: fileLinkCtrl, } privateAPI.GET("/files/upload-urls", fileHandler.GetUploadURLs) privateAPI.GET("/files/multipart-upload-urls", fileHandler.GetMultipartUploadURLs) @@ -776,7 +783,7 @@ func main() { setKnownAPIs(server.Routes()) setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl) setupAndStartCrons( - userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, + userAuthRepo, collectionLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl, embeddingController, healthCheckHandler, kexCtrl, castDb) @@ -905,7 +912,7 @@ func setupAndStartBackgroundJobs( objectCleanupController.StartClearingOrphanObjects() } -func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *public.PublicCollectionRepository, +func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRepo *public.CollectionLinkRepo, twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController, taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController, trashController *controller.TrashController, pushController *controller.PushController, @@ -931,7 +938,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR schedule(c, "@every 24h", func() { _ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30)) _ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7)) - _ = publicCollectionRepo.CleanupAccessHistory(context.Background()) + _ = collectionLinkRepo.CleanupAccessHistory(context.Background()) }) schedule(c, "@every 1m", func() { diff --git a/server/pkg/controller/public/collection_link.go b/server/pkg/controller/public/collection_link.go index bec8393425..52ca2d7689 100644 --- a/server/pkg/controller/public/collection_link.go +++ b/server/pkg/controller/public/collection_link.go @@ -53,7 +53,7 @@ const ( type CollectionLinkController struct { FileController *controller.FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController - PublicCollectionRepo *public.PublicCollectionRepository + CollectionLinkRepo *public.CollectionLinkRepo CollectionRepo *repo.CollectionRepository UserRepo *repo.UserRepository JwtSecret []byte @@ -61,11 +61,11 @@ type CollectionLinkController struct { func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) { accessToken := shortuuid.New()[0:AccessTokenLength] - err := c.PublicCollectionRepo. + err := c.CollectionLinkRepo. 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}) + collectionToPubUrlMap, err2 := c.CollectionLinkRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID}) if err2 != nil { return ente.PublicURL{}, stacktrace.Propagate(err2, "") } @@ -81,7 +81,7 @@ func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.Crea } } response := ente.PublicURL{ - URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken), + URL: c.CollectionLinkRepo.GetAlbumUrl(accessToken), ValidTill: req.ValidTill, DeviceLimit: req.DeviceLimit, EnableDownload: true, @@ -92,7 +92,7 @@ func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.Crea } func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) { - return c.PublicCollectionRepo.GetActiveCollectionLinkRow(ctx, collectionID) + return c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, collectionID) } func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) { @@ -119,12 +119,12 @@ func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, // Disable all public accessTokens generated for the given cID till date. func (c *CollectionLinkController) Disable(ctx context.Context, cID int64) error { - err := c.PublicCollectionRepo.DisableSharing(ctx, cID) + err := c.CollectionLinkRepo.DisableSharing(ctx, cID) return stacktrace.Propagate(err, "") } func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) { - publicCollectionToken, err := c.PublicCollectionRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID) + publicCollectionToken, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID) if err != nil { return ente.PublicURL{}, err } @@ -154,12 +154,12 @@ func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente if req.EnableJoin != nil { publicCollectionToken.EnableJoin = *req.EnableJoin } - err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken) + err = c.CollectionLinkRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken) if err != nil { return ente.PublicURL{}, stacktrace.Propagate(err, "") } return ente.PublicURL{ - URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token), + URL: c.CollectionLinkRepo.GetAlbumUrl(publicCollectionToken.Token), DeviceLimit: publicCollectionToken.DeviceLimit, ValidTill: publicCollectionToken.ValidTill, EnableDownload: publicCollectionToken.EnableDownload, @@ -178,7 +178,7 @@ func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente // attack for guessing password. func (c *CollectionLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) { accessContext := auth.MustGetPublicAccessContext(ctx) - collectionLinkRow, err := c.PublicCollectionRepo.GetActiveCollectionLinkRow(ctx, accessContext.CollectionID) + collectionLinkRow, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, accessContext.CollectionID) if err != nil { return nil, stacktrace.Propagate(err, "failed to get public collection info") } @@ -200,11 +200,11 @@ func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseR } logrus.WithField("collectionID", accessContext.CollectionID).Error("CRITICAL: received abuse report") - err := c.PublicCollectionRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details) + err := c.CollectionLinkRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details) if err != nil { return stacktrace.Propagate(err, "") } - count, err := c.PublicCollectionRepo.GetAbuseReportCount(ctx, accessContext) + count, err := c.CollectionLinkRepo.GetAbuseReportCount(ctx, accessContext) if err != nil { return stacktrace.Propagate(err, "") } @@ -259,7 +259,7 @@ func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, rep func (c *CollectionLinkController) 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) + collectionIDs, err := c.CollectionLinkRepo.GetActivePublicTokenForUser(ctx, userID) if err != nil { return stacktrace.Propagate(err, "") } diff --git a/server/pkg/middleware/collection_token.go b/server/pkg/middleware/collection_token.go index 37ff02458e..1760718e65 100644 --- a/server/pkg/middleware/collection_token.go +++ b/server/pkg/middleware/collection_token.go @@ -28,7 +28,7 @@ var whitelistedCollectionShareIDs = []int64{111} // CollectionTokenMiddleware intercepts and authenticates incoming requests type CollectionTokenMiddleware struct { - PublicCollectionRepo *public.PublicCollectionRepository + CollectionLinkRepo *public.CollectionLinkRepo PublicCollectionCtrl *public2.CollectionLinkController CollectionRepo *repo.CollectionRepository Cache *cache.Cache @@ -54,7 +54,7 @@ func (m *CollectionTokenMiddleware) Authenticate(urlSanitizer func(_ *gin.Contex cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":") cachedValue, cacheHit := m.Cache.Get(cacheKey) if !cacheHit { - publicCollectionSummary, err = m.PublicCollectionRepo.GetCollectionSummaryByToken(c, accessToken) + publicCollectionSummary, err = m.CollectionLinkRepo.GetCollectionSummaryByToken(c, accessToken) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return @@ -130,7 +130,7 @@ func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, } sharedID := collectionSummary.ID - hasAccessedInPast, err := m.PublicCollectionRepo.AccessedInPast(ctx, sharedID, ip, ua) + hasAccessedInPast, err := m.CollectionLinkRepo.AccessedInPast(ctx, sharedID, ip, ua) if err != nil { return false, stacktrace.Propagate(err, "") } @@ -138,7 +138,7 @@ func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, if hasAccessedInPast { return false, nil } - count, err := m.PublicCollectionRepo.GetUniqueAccessCount(ctx, sharedID) + count, err := m.CollectionLinkRepo.GetUniqueAccessCount(ctx, sharedID) if err != nil { return false, stacktrace.Propagate(err, "failed to get unique access count") } @@ -159,7 +159,7 @@ func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, if deviceLimit > 0 && count >= deviceLimit { return true, nil } - err = m.PublicCollectionRepo.RecordAccessHistory(ctx, sharedID, ip, ua) + err = m.CollectionLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua) return false, stacktrace.Propagate(err, "failed to record access history") } diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go index 40acc98b8a..c76b40da50 100644 --- a/server/pkg/repo/collection.go +++ b/server/pkg/repo/collection.go @@ -23,13 +23,13 @@ import ( // CollectionRepository defines the methods for inserting, updating and // retrieving collection entities from the underlying repository type CollectionRepository struct { - DB *sql.DB - FileRepo *FileRepository - PublicCollectionRepo *public.PublicCollectionRepository - TrashRepo *TrashRepository - SecretEncryptionKey []byte - QueueRepo *QueueRepository - LatencyLogger *prometheus.HistogramVec + DB *sql.DB + FileRepo *FileRepository + CollectionLinkRepo *public.CollectionLinkRepo + TrashRepo *TrashRepository + SecretEncryptionKey []byte + QueueRepo *QueueRepository + LatencyLogger *prometheus.HistogramVec } type SharedCollection struct { @@ -75,7 +75,7 @@ func (repo *CollectionRepository) Get(collectionID int64) (ente.Collection, erro c.EncryptedName = encryptedName.String c.NameDecryptionNonce = nameDecryptionNonce.String } - urlMap, err := repo.PublicCollectionRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID}) + urlMap, err := repo.CollectionLinkRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID}) if err != nil { return ente.Collection{}, stacktrace.Propagate(err, "failed to get publicURL info") } @@ -175,7 +175,7 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_ if _, ok := addPublicUrlMap[pctToken.String]; !ok { addPublicUrlMap[pctToken.String] = true url := ente.PublicURL{ - URL: repo.PublicCollectionRepo.GetAlbumUrl(pctToken.String), + URL: repo.CollectionLinkRepo.GetAlbumUrl(pctToken.String), DeviceLimit: int(pctDeviceLimit.Int32), ValidTill: pctValidTill.Int64, EnableDownload: pctEnableDownload.Bool, diff --git a/server/pkg/repo/public/public_collection.go b/server/pkg/repo/public/collection_link.go similarity index 80% rename from server/pkg/repo/public/public_collection.go rename to server/pkg/repo/public/collection_link.go index 5a73c2e43f..fafcd4cb11 100644 --- a/server/pkg/repo/public/public_collection.go +++ b/server/pkg/repo/public/collection_link.go @@ -13,29 +13,29 @@ import ( const BaseShareURL = "https://albums.ente.io/?t=%s" -// PublicCollectionRepository defines the methods for inserting, updating and +// CollectionLinkRepo defines the methods for inserting, updating and // retrieving entities related to public collections -type PublicCollectionRepository struct { +type CollectionLinkRepo struct { DB *sql.DB albumHost string } -// NewPublicCollectionRepository .. -func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository { +// NewCollectionLinkRepository .. +func NewCollectionLinkRepository(db *sql.DB, albumHost string) *CollectionLinkRepo { if albumHost == "" { albumHost = "https://albums.ente.io" } - return &PublicCollectionRepository{ + return &CollectionLinkRepo{ DB: db, albumHost: albumHost, } } -func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string { +func (pcr *CollectionLinkRepo) GetAlbumUrl(token string) string { return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token) } -func (pcr *PublicCollectionRepository) Insert(ctx context.Context, +func (pcr *CollectionLinkRepo) Insert(ctx context.Context, cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error { // default value for enableJoin is true join := true @@ -51,7 +51,7 @@ func (pcr *PublicCollectionRepository) Insert(ctx context.Context, return stacktrace.Propagate(err, "failed to insert") } -func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID int64) error { +func (pcr *CollectionLinkRepo) DisableSharing(ctx context.Context, cID int64) error { _, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET is_disabled = true where collection_id = $1 and is_disabled = false`, cID) return stacktrace.Propagate(err, "failed to disable sharing") @@ -59,7 +59,7 @@ func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID i // 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 *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) { +func (pcr *CollectionLinkRepo) 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)) @@ -94,7 +94,7 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con // GetActiveCollectionLinkRow will return ente.CollectionLinkRow for given collection ID // Note: The token could be expired or deviceLimit is already reached -func (pcr *PublicCollectionRepository) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) { +func (pcr *CollectionLinkRepo) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, 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`, @@ -111,7 +111,7 @@ func (pcr *PublicCollectionRepository) GetActiveCollectionLinkRow(ctx context.Co } // UpdatePublicCollectionToken will update the row for corresponding public collection token -func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) error { +func (pcr *CollectionLinkRepo) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) 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`, @@ -119,7 +119,7 @@ func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.C return stacktrace.Propagate(err, "failed to update public collection token") } -func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext, +func (pcr *CollectionLinkRepo) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext, 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) @@ -128,7 +128,7 @@ func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, ac return stacktrace.Propagate(err, "failed to record abuse report") } -func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) { +func (pcr *CollectionLinkRepo) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_abuse_report WHERE share_id = $1`, accessCtx.ID) var count int64 = 0 err := row.Scan(&count) @@ -138,7 +138,7 @@ func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context, return count, nil } -func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) { +func (pcr *CollectionLinkRepo) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_collection_access_history WHERE share_id = $1`, shareId) var count int64 = 0 err := row.Scan(&count) @@ -148,7 +148,7 @@ func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context, return count, nil } -func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { +func (pcr *CollectionLinkRepo) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error { _, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_collection_access_history (share_id, ip, user_agent) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT unique_access_sid_ip_ua DO NOTHING;`, @@ -157,7 +157,7 @@ func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context, } // AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past -func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) { +func (pcr *CollectionLinkRepo) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) { row := pcr.DB.QueryRowContext(ctx, `select share_id from public_collection_access_history where share_id =$1 and ip = $2 and user_agent = $3`, shareID, ip, ua) var tempID int64 @@ -168,7 +168,7 @@ func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, share return true, stacktrace.Propagate(err, "failed to record access history") } -func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) { +func (pcr *CollectionLinkRepo) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) { row := pcr.DB.QueryRowContext(ctx, `SELECT sct.id, sct.collection_id, sct.is_disabled, sct.valid_till, sct.device_limit, sct.pw_hash, sct.created_at, sct.updated_at, count(ah.share_id) @@ -185,7 +185,7 @@ func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.C return result, nil } -func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) { +func (pcr *CollectionLinkRepo) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) { rows, err := pcr.DB.QueryContext(ctx, `select pt.collection_id from public_collection_tokens pt left join collections c on pt.collection_id = c.collection_id where pt.is_disabled = FALSE and c.owner_id= $1;`, userID) if err != nil { return nil, stacktrace.Propagate(err, "") @@ -204,7 +204,7 @@ func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.C } // CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days -func (pcr *PublicCollectionRepository) CleanupAccessHistory(ctx context.Context) error { +func (pcr *CollectionLinkRepo) 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") diff --git a/server/pkg/repo/public/public_file.go b/server/pkg/repo/public/file_link.go similarity index 100% rename from server/pkg/repo/public/public_file.go rename to server/pkg/repo/public/file_link.go From 3aa419b43027dda9cfba7d463b614accf30172e2 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:23:55 +0530 Subject: [PATCH 13/57] Add config for locker url --- server/cmd/museum/main.go | 4 +++- server/configurations/local.yaml | 7 ++++++- server/pkg/repo/public/file_link.go | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index bda540113e..f0b8c2c371 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -98,6 +98,7 @@ func main() { } viper.SetDefault("apps.public-albums", "https://albums.ente.io") + viper.SetDefault("apps.public-locker", "https://locker.ente.io") viper.SetDefault("apps.accounts", "https://accounts.ente.io") viper.SetDefault("apps.cast", "https://cast.ente.io") viper.SetDefault("apps.family", "https://family.ente.io") @@ -179,6 +180,7 @@ func main() { familyRepo := &repo.FamilyRepository{DB: db} trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo} collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums")) + fileLinkRepo := public.NewFileLinkRepo(db) collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo, TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger} pushRepo := &repo.PushTokenRepository{DB: db} @@ -430,7 +432,7 @@ func main() { } fileLinkCtrl := &publicCtrl.FileLinkController{ FileController: fileController, - FileLinkRepo: nil, + FileLinkRepo: fileLinkRepo, FileRepo: fileRepo, JwtSecret: jwtSecretBytes, } diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index b6b1d567eb..a16f560e43 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -79,9 +79,14 @@ http: apps: # Default is https://albums.ente.io # - # If you're running a self hosted instance and wish to serve public links, + # If you're running a self hosted instance and wish to serve public links for photos, # set this to the URL where your albums web app is running. public-albums: + # Default is https://locker.ente.io + # + # If you're running a self-hosted instance and wish to serve public links for locker, + # set this to the URL where your albums web app is running. + public-locker: # Default is https://cast.ente.io cast: # Default is https://accounts.ente.io diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go index 3f6dbf7b33..c03b74c693 100644 --- a/server/pkg/repo/public/file_link.go +++ b/server/pkg/repo/public/file_link.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/ente-io/museum/ente/base" + "github.com/spf13/viper" "github.com/ente-io/museum/ente" "github.com/ente-io/stacktrace" @@ -20,10 +21,12 @@ type FileLinkRepository struct { } // NewFileLinkRepo .. -func NewFileLinkRepo(db *sql.DB, albumHost string, lockerHost string) *FileLinkRepository { +func NewFileLinkRepo(db *sql.DB) *FileLinkRepository { + albumHost := viper.GetString("apps.public-albums") if albumHost == "" { albumHost = "https://albums.ente.io" } + lockerHost := viper.GetString("apps.public-locker") if lockerHost == "" { lockerHost = "https://locker.ente.io" } From dbb1ad66d359ed38c65f2726e2cc69c103acfb84 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:50:57 +0530 Subject: [PATCH 14/57] Rename and minor fixes --- server/cmd/museum/main.go | 25 +++++++++++------ ...collection_token.go => collection_link.go} | 12 ++++---- .../{file_link_token.go => file_link.go} | 28 +++++++------------ server/pkg/middleware/rate_limit.go | 1 + 4 files changed, 34 insertions(+), 32 deletions(-) rename server/pkg/middleware/{collection_token.go => collection_link.go} (93%) rename server/pkg/middleware/{file_link_token.go => file_link.go} (87%) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index f0b8c2c371..5533a17563 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -354,6 +354,12 @@ func main() { userCache, userCacheCtrl, ) + fileLinkCtrl := &publicCtrl.FileLinkController{ + FileController: fileController, + FileLinkRepo: fileLinkRepo, + FileRepo: fileRepo, + JwtSecret: jwtSecretBytes, + } passkeyCtrl := &controller.PasskeyController{ Repo: passkeysRepo, @@ -361,7 +367,7 @@ func main() { } authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController} - collectionTokenMiddleware := middleware.CollectionTokenMiddleware{ + collectionLinkMiddleware := middleware.CollectionLinkMiddleware{ CollectionLinkRepo: collectionLinkRepo, PublicCollectionCtrl: collectionLinkCtrl, CollectionRepo: collectionRepo, @@ -369,6 +375,13 @@ func main() { BillingCtrl: billingController, DiscordController: discordController, } + fileLinkMiddleware := &middleware.FileLinkMiddleware{ + FileLinkRepo: fileLinkRepo, + FileLinkCtrl: fileLinkCtrl, + Cache: accessTokenCache, + BillingCtrl: billingController, + DiscordController: discordController, + } if environment != "local" { gin.SetMode(gin.ReleaseMode) @@ -407,7 +420,9 @@ func main() { familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer)) publicCollectionAPI := server.Group("/public-collection") - publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionTokenMiddleware.Authenticate(urlSanitizer)) + publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionLinkMiddleware.Authenticate(urlSanitizer)) + fileLinkApi := server.GET("/file-link") + fileLinkApi.Use(rateLimiter.GlobalRateLimiter(), fileLinkMiddleware.Authenticate(urlSanitizer)) healthCheckHandler := &api.HealthCheckHandler{ DB: db, @@ -430,12 +445,6 @@ func main() { ObjectRepo: objectRepo, FileRepo: fileRepo, } - fileLinkCtrl := &publicCtrl.FileLinkController{ - FileController: fileController, - FileLinkRepo: fileLinkRepo, - FileRepo: fileRepo, - JwtSecret: jwtSecretBytes, - } fileHandler := &api.FileHandler{ Controller: fileController, diff --git a/server/pkg/middleware/collection_token.go b/server/pkg/middleware/collection_link.go similarity index 93% rename from server/pkg/middleware/collection_token.go rename to server/pkg/middleware/collection_link.go index 1760718e65..5b11a5ec07 100644 --- a/server/pkg/middleware/collection_token.go +++ b/server/pkg/middleware/collection_link.go @@ -26,8 +26,8 @@ import ( var passwordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"} var whitelistedCollectionShareIDs = []int64{111} -// CollectionTokenMiddleware intercepts and authenticates incoming requests -type CollectionTokenMiddleware struct { +// CollectionLinkMiddleware intercepts and authenticates incoming requests +type CollectionLinkMiddleware struct { CollectionLinkRepo *public.CollectionLinkRepo PublicCollectionCtrl *public2.CollectionLinkController CollectionRepo *repo.CollectionRepository @@ -39,7 +39,7 @@ type CollectionTokenMiddleware struct { // Authenticate returns a middle ware that extracts the `X-Auth-Access-Token` // within the header of a request and uses it to validate the access token and set the // ente.PublicAccessContext with auth.PublicAccessKey as key -func (m *CollectionTokenMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { +func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc { return func(c *gin.Context) { accessToken := auth.GetAccessToken(c) if accessToken == "" { @@ -114,7 +114,7 @@ func (m *CollectionTokenMiddleware) Authenticate(urlSanitizer func(_ *gin.Contex c.Next() } } -func (m *CollectionTokenMiddleware) validateOwnersSubscription(cID int64) error { +func (m *CollectionLinkMiddleware) validateOwnersSubscription(cID int64) error { userID, err := m.CollectionRepo.GetOwnerID(cID) if err != nil { return stacktrace.Propagate(err, "") @@ -122,7 +122,7 @@ func (m *CollectionTokenMiddleware) validateOwnersSubscription(cID int64) error return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, false) } -func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, +func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context, collectionSummary ente.PublicCollectionSummary, ip string, ua string) (bool, error) { // skip deviceLimit check & record keeping for requests via CF worker if network.IsCFWorkerIP(ip) { @@ -164,7 +164,7 @@ func (m *CollectionTokenMiddleware) isDeviceLimitReached(ctx context.Context, } // validatePassword will verify if the user is provided correct password for the public album -func (m *CollectionTokenMiddleware) validatePassword(c *gin.Context, reqPath string, +func (m *CollectionLinkMiddleware) validatePassword(c *gin.Context, reqPath string, collectionSummary ente.PublicCollectionSummary) error { if array.StringInList(reqPath, passwordWhiteListedURLs) { return nil diff --git a/server/pkg/middleware/file_link_token.go b/server/pkg/middleware/file_link.go similarity index 87% rename from server/pkg/middleware/file_link_token.go rename to server/pkg/middleware/file_link.go index 539ca1179e..d861e9f0f4 100644 --- a/server/pkg/middleware/file_link_token.go +++ b/server/pkg/middleware/file_link.go @@ -5,13 +5,12 @@ import ( "fmt" publicCtrl "github.com/ente-io/museum/pkg/controller/public" "github.com/ente-io/museum/pkg/repo/public" + "github.com/ente-io/museum/pkg/utils/array" "net/http" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/controller/discord" - "github.com/ente-io/museum/pkg/repo" - "github.com/ente-io/museum/pkg/utils/array" "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/museum/pkg/utils/network" "github.com/ente-io/museum/pkg/utils/time" @@ -21,13 +20,12 @@ import ( "github.com/sirupsen/logrus" ) -var filePasswordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"} +var filePasswordWhiteListedURLs = []string{"/file-link/info", "/file-link/verify-password"} // FileLinkMiddleware intercepts and authenticates incoming requests type FileLinkMiddleware struct { FileLinkRepo *public.FileLinkRepository FileLinkCtrl *publicCtrl.FileLinkController - CollectionRepo *repo.CollectionRepository Cache *cache.Cache BillingCtrl *controller.BillingController DiscordController *discord.DiscordController @@ -53,6 +51,7 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri if !cacheHit { fileLinkRow, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken) if err != nil { + logrus.WithError(err).Info("failed to get file link row by token") c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } @@ -62,15 +61,15 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri } // validate if user still has active paid subscription if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(fileLinkRow.OwnerID, true); err != nil { - logrus.WithError(err).Warn("failed to verify active paid subscription") + logrus.WithError(err).Info("failed to verify active paid subscription") c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"}) return } // validate device limit - reached, err := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent) - if err != nil { - logrus.WithError(err).Error("failed to check device limit") + reached, limitErr := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent) + if limitErr != nil { + logrus.WithError(limitErr).Error("failed to check device limit") c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"}) return } @@ -111,13 +110,6 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri c.Next() } } -func (m *FileLinkMiddleware) validateOwnersSubscription(cID int64) error { - userID, err := m.CollectionRepo.GetOwnerID(cID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) -} func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context, collectionSummary *ente.FileLinkRow, ip string, ua string) (bool, error) { @@ -161,11 +153,11 @@ func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context, // validatePassword will verify if the user is provided correct password for the public album func (m *FileLinkMiddleware) validatePassword(c *gin.Context, reqPath string, fileLinkRow *ente.FileLinkRow) error { - if array.StringInList(reqPath, passwordWhiteListedURLs) { - return nil - } accessTokenJWT := auth.GetAccessTokenJWT(c) if accessTokenJWT == "" { + if array.StringInList(reqPath, filePasswordWhiteListedURLs) { + return nil + } return ente.ErrAuthenticationRequired } return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash) diff --git a/server/pkg/middleware/rate_limit.go b/server/pkg/middleware/rate_limit.go index 14d3c92a00..bf4c403cfe 100644 --- a/server/pkg/middleware/rate_limit.go +++ b/server/pkg/middleware/rate_limit.go @@ -140,6 +140,7 @@ func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limi reqPath == "/users/verify-email" || reqPath == "/user/change-email" || reqPath == "/public-collection/verify-password" || + reqPath == "/file-link/verify-password" || reqPath == "/family/accept-invite" || reqPath == "/users/srp/attributes" || (reqPath == "/cast/device-info" && reqMethod == "POST") || From 2d0d914fd3d9f4c57fb390c2953d6a138c92e066 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:27:02 +0530 Subject: [PATCH 15/57] Hook APIs to get file or thumbnail from fileLink --- server/cmd/museum/main.go | 4 ++++ server/ente/file_link.go | 1 + server/pkg/api/file_url.go | 25 +++++++++++++++++++++++++ server/pkg/middleware/file_link.go | 8 ++++++-- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 5533a17563..e006f9c2a5 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -597,6 +597,10 @@ func main() { StorageBonusController: storageBonusCtrl, } + fileLinkApi.GET("/info", fileHandler.LinkInfo) + fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail) + fileLinkApi.GET("/file", fileHandler.LinkFile) + publicCollectionAPI.GET("/files/preview/:fileID", publicCollectionHandler.GetThumbnail) publicCollectionAPI.GET("/files/download/:fileID", publicCollectionHandler.GetFile) publicCollectionAPI.GET("/files/data/fetch", publicCollectionHandler.GetFileData) diff --git a/server/ente/file_link.go b/server/ente/file_link.go index a69034bf11..e817e35da0 100644 --- a/server/ente/file_link.go +++ b/server/ente/file_link.go @@ -90,4 +90,5 @@ type FileLinkAccessContext struct { IP string UserAgent string FileID int64 + OwnerID int64 } diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go index 1780345bfc..aefb47daba 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_url.go @@ -2,6 +2,7 @@ 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" @@ -25,6 +26,30 @@ func (h *FileHandler) ShareUrl(c *gin.Context) { c.JSON(http.StatusOK, response) } +func (h *FileHandler) LinkInfo(c *gin.Context) { + +} + +func (h *FileHandler) LinkThumbnail(c *gin.Context) { + linkCtx := auth.MustGetFileLinkAccessContext(c) + url, err := h.Controller.GetThumbnailURL(c, linkCtx.OwnerID, linkCtx.FileID) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.Redirect(http.StatusTemporaryRedirect, url) +} + +func (h *FileHandler) LinkFile(c *gin.Context) { + linkCtx := auth.MustGetFileLinkAccessContext(c) + url, err := h.Controller.GetFileURL(c, linkCtx.OwnerID, linkCtx.FileID) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.Redirect(http.StatusTemporaryRedirect, url) +} + func (h *FileHandler) DisableUrl(c *gin.Context) { cID, err := strconv.ParseInt(c.Param("fileID"), 10, 64) if err != nil { diff --git a/server/pkg/middleware/file_link.go b/server/pkg/middleware/file_link.go index d861e9f0f4..e5dcf48a3d 100644 --- a/server/pkg/middleware/file_link.go +++ b/server/pkg/middleware/file_link.go @@ -106,6 +106,7 @@ func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) stri IP: clientIP, UserAgent: userAgent, FileID: fileLinkRow.FileID, + OwnerID: fileLinkRow.OwnerID, }) c.Next() } @@ -151,8 +152,11 @@ func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context, } // validatePassword will verify if the user is provided correct password for the public album -func (m *FileLinkMiddleware) validatePassword(c *gin.Context, reqPath string, - fileLinkRow *ente.FileLinkRow) error { +func (m *FileLinkMiddleware) validatePassword( + c *gin.Context, + reqPath string, + fileLinkRow *ente.FileLinkRow, +) error { accessTokenJWT := auth.GetAccessTokenJWT(c) if accessTokenJWT == "" { if array.StringInList(reqPath, filePasswordWhiteListedURLs) { From 02b93b12fc4d16cf2c97304f27a982c6ab435a9c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:54:50 +0530 Subject: [PATCH 16/57] Fix typo --- server/cmd/museum/main.go | 2 +- server/pkg/api/file_url.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index e006f9c2a5..b017671e63 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -462,7 +462,7 @@ func main() { privateAPI.POST("files/share-url", fileHandler.ShareUrl) privateAPI.PUT("files/share-url", fileHandler.UpdateFileURL) privateAPI.DELETE("files/share-url/:fileID", fileHandler.DisableUrl) - privateAPI.GET("files/share-urls/", fileHandler.DisableUrl) + privateAPI.GET("files/share-urls/", fileHandler.GetUrls) privateAPI.PUT("/files/data", fileHandler.PutFileData) privateAPI.PUT("/files/video-data", fileHandler.PutVideoData) diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go index aefb47daba..2c2ea87af7 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_url.go @@ -67,7 +67,7 @@ func (h *FileHandler) DisableUrl(c *gin.Context) { func (h *FileHandler) GetUrls(c *gin.Context) { sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64) if err != nil { - handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "")) + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "sinceTime parsing failed")) return } limit := 500 From eb8737cb469c2be2519c74ed80ace0f9fb415b9f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:58:40 +0530 Subject: [PATCH 17/57] Add verify password endpoint --- server/cmd/museum/main.go | 1 + server/pkg/api/file_url.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index b017671e63..430e2c2c9d 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -600,6 +600,7 @@ func main() { fileLinkApi.GET("/info", fileHandler.LinkInfo) fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail) fileLinkApi.GET("/file", fileHandler.LinkFile) + fileLinkApi.POST("/verify-password", fileHandler.VerifyPassword) publicCollectionAPI.GET("/files/preview/:fileID", publicCollectionHandler.GetThumbnail) publicCollectionAPI.GET("/files/download/:fileID", publicCollectionHandler.GetFile) diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_url.go index 2c2ea87af7..f25e5750b5 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_url.go @@ -88,6 +88,21 @@ func (h *FileHandler) GetUrls(c *gin.Context) { }) } +// VerifyPassword verifies the password for given public access token and return signed jwt token if it's valid +func (h *FileHandler) VerifyPassword(c *gin.Context) { + var req ente.VerifyPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + resp, err := h.FileUrlCtrl.VerifyPassword(c, req) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, resp) +} + // UpdateFileURL updates the share URL for a file func (h *FileHandler) UpdateFileURL(c *gin.Context) { var req ente.UpdateFileUrl From e69276cf5fc161fc26ee2a82e24c2d801f2487b6 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:00:15 +0530 Subject: [PATCH 18/57] Rename --- server/pkg/api/{file_url.go => file_link.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename server/pkg/api/{file_url.go => file_link.go} (96%) diff --git a/server/pkg/api/file_url.go b/server/pkg/api/file_link.go similarity index 96% rename from server/pkg/api/file_url.go rename to server/pkg/api/file_link.go index f25e5750b5..0a0686f71a 100644 --- a/server/pkg/api/file_url.go +++ b/server/pkg/api/file_link.go @@ -88,7 +88,7 @@ func (h *FileHandler) GetUrls(c *gin.Context) { }) } -// VerifyPassword verifies the password for given public access token and return signed jwt token if it's valid +// VerifyPassword verifies the password for given link access token and return signed jwt token if it's valid func (h *FileHandler) VerifyPassword(c *gin.Context) { var req ente.VerifyPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { From 8b6d7e049a97a6c4005878901ee0bc344a5fbc94 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:22:36 +0530 Subject: [PATCH 19/57] Remove link when files are trashed --- server/cmd/museum/main.go | 5 +++-- server/pkg/repo/trash.go | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 430e2c2c9d..3e08aeb253 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -176,11 +176,12 @@ func main() { fileRepo := &repo.FileRepository{DB: db, S3Config: s3Config, QueueRepo: queueRepo, ObjectRepo: objectRepo, ObjectCleanupRepo: objectCleanupRepo, ObjectCopiesRepo: objectCopiesRepo, UsageRepo: usageRepo} + fileLinkRepo := public.NewFileLinkRepo(db) fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo} familyRepo := &repo.FamilyRepository{DB: db} - trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo} + trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo, FileLinkRepo: fileLinkRepo} collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums")) - fileLinkRepo := public.NewFileLinkRepo(db) + collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo, TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger} pushRepo := &repo.PushTokenRepository{DB: db} diff --git a/server/pkg/repo/trash.go b/server/pkg/repo/trash.go index 1c6ab6b45f..b79a40a1aa 100644 --- a/server/pkg/repo/trash.go +++ b/server/pkg/repo/trash.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/ente-io/museum/pkg/repo/public" "strings" "github.com/ente-io/museum/ente" @@ -32,10 +33,11 @@ type FileWithUpdatedAt struct { } type TrashRepository struct { - DB *sql.DB - ObjectRepo *ObjectRepository - FileRepo *FileRepository - QueueRepo *QueueRepository + DB *sql.DB + ObjectRepo *ObjectRepository + FileRepo *FileRepository + QueueRepo *QueueRepository + FileLinkRepo *public.FileLinkRepository } func (t *TrashRepository) InsertItems(ctx context.Context, tx *sql.Tx, userID int64, items []ente.TrashItemRequest) error { @@ -156,6 +158,13 @@ func (t *TrashRepository) TrashFiles(fileIDs []int64, userID int64, trash ente.T return stacktrace.Propagate(err, "") } err = tx.Commit() + + if err == nil { + removeLinkErr := t.FileLinkRepo.DisableLinkForFiles(ctx, fileIDs) + if removeLinkErr != nil { + return stacktrace.Propagate(removeLinkErr, "failed to disable file links for files being trashed") + } + } return stacktrace.Propagate(err, "") } From e8e7f81593c9314be22b5513b40a4e4b75391200 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:25:27 +0530 Subject: [PATCH 20/57] Clean up old link history --- server/cmd/museum/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 3e08aeb253..17c7b5ba42 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -800,7 +800,7 @@ func main() { setKnownAPIs(server.Routes()) setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl) setupAndStartCrons( - userAuthRepo, collectionLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, + userAuthRepo, collectionLinkRepo, fileLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl, embeddingController, healthCheckHandler, kexCtrl, castDb) @@ -930,6 +930,7 @@ func setupAndStartBackgroundJobs( } func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRepo *public.CollectionLinkRepo, + fileLinkRepo *public.FileLinkRepository, twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController, taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController, trashController *controller.TrashController, pushController *controller.PushController, @@ -956,6 +957,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRep _ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30)) _ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7)) _ = collectionLinkRepo.CleanupAccessHistory(context.Background()) + _ = fileLinkRepo.CleanupAccessHistory(context.Background()) }) schedule(c, "@every 1m", func() { From c57d4679656b2b18883396cc19a35a5c86d3715f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:28:37 +0530 Subject: [PATCH 21/57] Disable all links on account deletion --- server/cmd/museum/main.go | 1 + server/pkg/controller/public/collection_link.go | 3 ++- server/pkg/repo/public/file_link.go | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 17c7b5ba42..9b22620181 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -308,6 +308,7 @@ func main() { FileController: fileController, EmailNotificationCtrl: emailNotificationCtrl, CollectionLinkRepo: collectionLinkRepo, + FileLinkRepo: fileLinkRepo, CollectionRepo: collectionRepo, UserRepo: userRepo, JwtSecret: jwtSecretBytes, diff --git a/server/pkg/controller/public/collection_link.go b/server/pkg/controller/public/collection_link.go index 52ca2d7689..ead744bfbf 100644 --- a/server/pkg/controller/public/collection_link.go +++ b/server/pkg/controller/public/collection_link.go @@ -54,6 +54,7 @@ type CollectionLinkController struct { FileController *controller.FileController EmailNotificationCtrl *emailCtrl.EmailNotificationController CollectionLinkRepo *public.CollectionLinkRepo + FileLinkRepo *public.FileLinkRepository CollectionRepo *repo.CollectionRepository UserRepo *repo.UserRepository JwtSecret []byte @@ -270,7 +271,7 @@ func (c *CollectionLinkController) HandleAccountDeletion(ctx context.Context, us return stacktrace.Propagate(err, "") } } - return nil + return c.FileLinkRepo.DisableLinksForUser(ctx, userID) } // GetPublicCollection will return collection info for a public url. diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go index c03b74c693..beaeda2e74 100644 --- a/server/pkg/repo/public/file_link.go +++ b/server/pkg/repo/public/file_link.go @@ -124,8 +124,8 @@ func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs } // DisableLinksForUser will disable all public file links for the given user -func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, linkID string) error { - _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE id = $1`, linkID) +func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, userID int64) error { + _, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE owner_id = $1`, userID) if err != nil { return stacktrace.Propagate(err, "failed to disable public file link") } From 6bed9bd8a28f3a5c2707276833fe92518cdbeac7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:44:22 +0530 Subject: [PATCH 22/57] Send file info --- server/pkg/api/file_link.go | 9 ++++++++- server/pkg/controller/public/file_link.go | 5 +++++ server/pkg/middleware/file_link.go | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/server/pkg/api/file_link.go b/server/pkg/api/file_link.go index 0a0686f71a..fa7dc9aaae 100644 --- a/server/pkg/api/file_link.go +++ b/server/pkg/api/file_link.go @@ -27,7 +27,14 @@ func (h *FileHandler) ShareUrl(c *gin.Context) { } func (h *FileHandler) LinkInfo(c *gin.Context) { - + resp, err := h.FileUrlCtrl.Info(c) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{ + "file": resp, + }) } func (h *FileHandler) LinkThumbnail(c *gin.Context) { diff --git a/server/pkg/controller/public/file_link.go b/server/pkg/controller/public/file_link.go index 39f1ba9fe7..dad8e139e9 100644 --- a/server/pkg/controller/public/file_link.go +++ b/server/pkg/controller/public/file_link.go @@ -110,6 +110,11 @@ func (c *FileLinkController) UpdateSharedUrl(ctx *gin.Context, req ente.UpdateFi return c.mapRowToFileUrl(ctx, fileLinkRow), nil } +func (c *FileLinkController) Info(ctx *gin.Context) (*ente.File, error) { + accessContext := auth.MustGetFileLinkAccessContext(ctx) + return c.FileRepo.GetFileAttributes(accessContext.FileID) +} + // 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 diff --git a/server/pkg/middleware/file_link.go b/server/pkg/middleware/file_link.go index e5dcf48a3d..6430382f2c 100644 --- a/server/pkg/middleware/file_link.go +++ b/server/pkg/middleware/file_link.go @@ -20,7 +20,7 @@ import ( "github.com/sirupsen/logrus" ) -var filePasswordWhiteListedURLs = []string{"/file-link/info", "/file-link/verify-password"} +var filePasswordWhiteListedURLs = []string{"/file-link/pass-info", "/file-link/verify-password"} // FileLinkMiddleware intercepts and authenticates incoming requests type FileLinkMiddleware struct { From d9710555ea8985562b2a1febb5f88b8fa27130db Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:48:28 +0530 Subject: [PATCH 23/57] Add endpoint to get pass-info --- server/cmd/museum/main.go | 1 + server/pkg/api/file_link.go | 13 +++++++++++++ server/pkg/controller/public/file_link.go | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 9b22620181..1a07b58c04 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -600,6 +600,7 @@ func main() { } fileLinkApi.GET("/info", fileHandler.LinkInfo) + fileLinkApi.GET("/pass-info", fileHandler.PasswordInfo) fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail) fileLinkApi.GET("/file", fileHandler.LinkFile) fileLinkApi.POST("/verify-password", fileHandler.VerifyPassword) diff --git a/server/pkg/api/file_link.go b/server/pkg/api/file_link.go index fa7dc9aaae..d243a87532 100644 --- a/server/pkg/api/file_link.go +++ b/server/pkg/api/file_link.go @@ -37,6 +37,19 @@ func (h *FileHandler) LinkInfo(c *gin.Context) { }) } +func (h *FileHandler) PasswordInfo(c *gin.Context) { + resp, err := h.FileUrlCtrl.PassInfo(c) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{ + "nonce": resp.Nonce, + "opsLimit": resp.OpsLimit, + "memLimit": resp.MemLimit, + }) +} + func (h *FileHandler) LinkThumbnail(c *gin.Context) { linkCtx := auth.MustGetFileLinkAccessContext(c) url, err := h.Controller.GetThumbnailURL(c, linkCtx.OwnerID, linkCtx.FileID) diff --git a/server/pkg/controller/public/file_link.go b/server/pkg/controller/public/file_link.go index dad8e139e9..be015bc719 100644 --- a/server/pkg/controller/public/file_link.go +++ b/server/pkg/controller/public/file_link.go @@ -115,6 +115,11 @@ func (c *FileLinkController) Info(ctx *gin.Context) (*ente.File, error) { return c.FileRepo.GetFileAttributes(accessContext.FileID) } +func (c *FileLinkController) PassInfo(ctx *gin.Context) (*ente.FileLinkRow, error) { + accessContext := auth.MustGetFileLinkAccessContext(ctx) + return c.FileLinkRepo.GetFileUrlRowByFileID(ctx, accessContext.FileID) +} + // 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 From e443838621f800d9089ce56f02d6b31b5a3d9412 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:51:23 +0530 Subject: [PATCH 24/57] Use diff statuscode when accessToken for password is missing --- server/ente/errors.go | 5 +++++ server/pkg/middleware/file_link.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/ente/errors.go b/server/ente/errors.go index 595b2f0417..2370ab7fe8 100644 --- a/server/ente/errors.go +++ b/server/ente/errors.go @@ -176,6 +176,11 @@ var ErrMaxPasskeysReached = ApiError{ Message: "Max passkeys limit reached", HttpStatusCode: http.StatusConflict, } +var ErrPassProtectedResource = ApiError{ + Code: "PASS_PROTECTED_RESOURCE", + Message: "This resource is password protected", + HttpStatusCode: http.StatusForbidden, +} var ErrCastPermissionDenied = ApiError{ Code: "CAST_PERMISSION_DENIED", diff --git a/server/pkg/middleware/file_link.go b/server/pkg/middleware/file_link.go index 6430382f2c..72b095bbc0 100644 --- a/server/pkg/middleware/file_link.go +++ b/server/pkg/middleware/file_link.go @@ -162,7 +162,7 @@ func (m *FileLinkMiddleware) validatePassword( if array.StringInList(reqPath, filePasswordWhiteListedURLs) { return nil } - return ente.ErrAuthenticationRequired + return &ente.ErrPassProtectedResource } return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash) } From 227ea4a3719ce67f3acf0c8d4fa02581dae1d845 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:07:01 +0530 Subject: [PATCH 25/57] Fix bugs --- server/cmd/museum/main.go | 8 ++++---- server/pkg/repo/public/file_link.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 1a07b58c04..87d720530a 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -461,10 +461,10 @@ 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/share-url", fileHandler.UpdateFileURL) - privateAPI.DELETE("files/share-url/:fileID", fileHandler.DisableUrl) - privateAPI.GET("files/share-urls/", fileHandler.GetUrls) + privateAPI.POST("/files/share-url", fileHandler.ShareUrl) + privateAPI.PUT("/files/share-url", fileHandler.UpdateFileURL) + privateAPI.DELETE("/files/share-url/:fileID", fileHandler.DisableUrl) + privateAPI.GET("/files/share-urls/", fileHandler.GetUrls) privateAPI.PUT("/files/data", fileHandler.PutFileData) privateAPI.PUT("/files/video-data", fileHandler.PutVideoData) diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go index beaeda2e74..b737ddfdf3 100644 --- a/server/pkg/repo/public/file_link.go +++ b/server/pkg/repo/public/file_link.go @@ -152,7 +152,7 @@ func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessT func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, - `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit + `SELECT id, file_id, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit, created_at, updated_at from public_file_tokens where file_id = $1 and is_disabled = FALSE`, fileID) From b64a69ebf0f7ed6a7d78336a53eda3615b65879e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:33:28 +0530 Subject: [PATCH 26/57] Fix minor bugs --- server/migrations/102_single_file_url.up.sql | 2 +- server/pkg/repo/public/file_link.go | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/server/migrations/102_single_file_url.up.sql b/server/migrations/102_single_file_url.up.sql index 0f94897ba2..3145e46aad 100644 --- a/server/migrations/102_single_file_url.up.sql +++ b/server/migrations/102_single_file_url.up.sql @@ -43,4 +43,4 @@ CREATE TABLE IF NOT EXISTS public_file_tokens_access_history CREATE UNIQUE INDEX IF NOT EXISTS public_file_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE; CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at); - +CREATE UNIQUE INDEX IF NOT EXISTS public_active_file_link_unique_idx ON public_file_tokens (file_id, is_disabled) WHERE is_disabled = FALSE; diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go index b737ddfdf3..9affce0f57 100644 --- a/server/pkg/repo/public/file_link.go +++ b/server/pkg/repo/public/file_link.go @@ -31,8 +31,9 @@ func NewFileLinkRepo(db *sql.DB) *FileLinkRepository { lockerHost = "https://locker.ente.io" } return &FileLinkRepository{ - DB: db, - photoHost: albumHost, + DB: db, + photoHost: albumHost, + lockerHost: lockerHost, } } @@ -59,7 +60,7 @@ func (pcr *FileLinkRepository) Insert( (id, file_id, owner_id, access_token, app) VALUES ($1, $2, $3, $4, $5)`, id, fileID, ownerID, token, string(app)) if err != nil { - if err.Error() == "pq: duplicate key value violates unique constraint \"public_file_token_unique_idx\"" { + if err.Error() == "pq: duplicate key value violates unique constraint \"public_active_file_link_unique_idx\"" { return nil, ente.ErrActiveLinkAlreadyExists } return nil, stacktrace.Propagate(err, "failed to insert") @@ -152,12 +153,12 @@ func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessT func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) { row := pcr.DB.QueryRowContext(ctx, - `SELECT id, file_id, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit, + `SELECT id, file_id, access_token, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit, created_at, updated_at from public_file_tokens where file_id = $1 and is_disabled = FALSE`, fileID) var result = ente.FileLinkRow{} - err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt) + err := row.Scan(&result.LinkID, &result.FileID, &result.Token, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt) if err != nil { if err == sql.ErrNoRows { return nil, ente.ErrNotFound From 04e3ad2b779955f806f96bdc2fb404a44cb73c7c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:15:26 +0530 Subject: [PATCH 27/57] Fix query bug in delete --- server/pkg/repo/public/file_link.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/pkg/repo/public/file_link.go b/server/pkg/repo/public/file_link.go index 9affce0f57..fbb2f4e072 100644 --- a/server/pkg/repo/public/file_link.go +++ b/server/pkg/repo/public/file_link.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/ente-io/museum/ente/base" + "github.com/lib/pq" "github.com/spf13/viper" "github.com/ente-io/museum/ente" @@ -117,7 +118,7 @@ func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs return nil } query := `UPDATE public_file_tokens SET is_disabled = TRUE WHERE file_id = ANY($1)` - _, err := pcr.DB.ExecContext(ctx, query, fileIDs) + _, err := pcr.DB.ExecContext(ctx, query, pq.Array(fileIDs)) if err != nil { return stacktrace.Propagate(err, "failed to disable public file links") } From fcc90c672540122c1e8956fa0c003eefa43d8a4f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:05:16 +0530 Subject: [PATCH 28/57] Bump version --- ...{102_single_file_url.down.sql => 103_single_file_url.down.sql} | 0 .../{102_single_file_url.up.sql => 103_single_file_url.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename server/migrations/{102_single_file_url.down.sql => 103_single_file_url.down.sql} (100%) rename server/migrations/{102_single_file_url.up.sql => 103_single_file_url.up.sql} (100%) diff --git a/server/migrations/102_single_file_url.down.sql b/server/migrations/103_single_file_url.down.sql similarity index 100% rename from server/migrations/102_single_file_url.down.sql rename to server/migrations/103_single_file_url.down.sql diff --git a/server/migrations/102_single_file_url.up.sql b/server/migrations/103_single_file_url.up.sql similarity index 100% rename from server/migrations/102_single_file_url.up.sql rename to server/migrations/103_single_file_url.up.sql From 8da1f638e108c8fb7e106d5e1d9727935a692da9 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Thu, 31 Jul 2025 15:15:51 +0530 Subject: [PATCH 29/57] extract string + code refractor --- mobile/apps/photos/lib/l10n/intl_en.arb | 21 +++++- .../collections/collection_action_sheet.dart | 4 +- .../image_editor/image_editor_app_bar.dart | 11 ++-- .../image_editor_crop_rotate.dart | 5 +- .../image_editor_main_bottom_bar.dart | 11 ++-- .../image_editor/image_editor_paint_bar.dart | 3 +- .../image_editor/image_editor_text_bar.dart | 9 +-- .../lib/ui/viewer/file/detail_page.dart | 66 +------------------ 8 files changed, 45 insertions(+), 85 deletions(-) diff --git a/mobile/apps/photos/lib/l10n/intl_en.arb b/mobile/apps/photos/lib/l10n/intl_en.arb index 21998814da..017637a8b8 100644 --- a/mobile/apps/photos/lib/l10n/intl_en.arb +++ b/mobile/apps/photos/lib/l10n/intl_en.arb @@ -1808,5 +1808,24 @@ "automaticallyAnalyzeAndSplitGrouping": "We will automatically analyze the grouping to determine if there are multiple people present, and separate them out again. This may take a few seconds.", "layout": "Layout", "day": "Day", - "peopleAutoAddDesc": "Select the people you want to automatically add to the album" + "peopleAutoAddDesc": "Select the people you want to automatically add to the album", + "undo": "Undo", + "redo": "Redo", + "filter": "Filter", + "adjust": "Adjust", + "draw": "Draw", + "sticker": "Sticker", + "brushColor": "Brush Color", + "font": "Font", + "background": "Background", + "align": "Align", + "addedToAlbums": "{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}", + "@addedToAlbums": { + "description": "Message shown when items are added to albums", + "placeholders": { + "count": { + "type": "int" + } + } + } } \ No newline at end of file diff --git a/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart b/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart index d049be0102..64fe799e46 100644 --- a/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart +++ b/mobile/apps/photos/lib/ui/collections/collection_action_sheet.dart @@ -310,9 +310,7 @@ class _CollectionActionSheetState extends State { if (result) { showShortToast( context, - "Added successfully to " + - _selectedCollections.length.toString() + - " albums", + S.of(context).addedToAlbums(_selectedCollections.length), ); widget.selectedFiles?.clearAll(); } diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart index 03e84cf75d..730feb5d3a 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_app_bar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import "package:flutter_svg/svg.dart"; import "package:photos/ente_theme_data.dart"; -import "package:photos/theme/ente_theme.dart"; +import "package:photos/generated/l10n.dart"; + import "package:photos/theme/ente_theme.dart"; import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart"; class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget { @@ -43,7 +44,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget { enableUndo ? close() : Navigator.of(context).pop(); }, child: Text( - 'Cancel', + S.of(context).cancel, style: getEnteTextTheme(context).body, ), ), @@ -52,7 +53,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( - tooltip: 'Undo', + tooltip: S.of(context).undo, onPressed: () { undo != null ? undo!() : null; }, @@ -66,7 +67,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(width: 12), IconButton( - tooltip: 'Redo', + tooltip: S.of(context).redo, onPressed: () { redo != null ? redo!() : null; }, @@ -88,7 +89,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget { key: ValueKey(isMainEditor ? 'save_copy' : 'done'), onPressed: done, child: Text( - isMainEditor ? 'Save Copy' : 'Done', + isMainEditor ? S.of(context).saveCopy : S.of(context).done, style: getEnteTextTheme(context).body.copyWith( color: isMainEditor ? (enableUndo diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart index a222ed92e4..10333cd3ce 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_crop_rotate.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:flutter_svg/svg.dart"; +import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart"; @@ -113,7 +114,7 @@ class _ImageEditorCropRotateBarState extends State children: [ CircularIconButton( svgPath: "assets/image-editor/image-editor-crop-rotate.svg", - label: "Rotate", + label: S.of(context).rotate, onTap: () { widget.editor.rotate(); }, @@ -121,7 +122,7 @@ class _ImageEditorCropRotateBarState extends State const SizedBox(width: 6), CircularIconButton( svgPath: "assets/image-editor/image-editor-flip.svg", - label: "Flip", + label: S.of(context).flip, onTap: () { widget.editor.flip(); }, diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart index 3321e645c1..32dc0a5ed8 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart"; @@ -90,7 +91,7 @@ class ImageEditorMainBottomBarState extends State children: [ CircularIconButton( svgPath: "assets/image-editor/image-editor-crop.svg", - label: "Crop", + label: S.of(context).crop, onTap: () { widget.editor.openCropRotateEditor(); }, @@ -98,21 +99,21 @@ class ImageEditorMainBottomBarState extends State CircularIconButton( svgPath: "assets/image-editor/image-editor-filter.svg", - label: "Filter", + label: S.of(context).filter, onTap: () { widget.editor.openFilterEditor(); }, ), CircularIconButton( svgPath: "assets/image-editor/image-editor-tune.svg", - label: "Adjust", + label: S.of(context).adjust, onTap: () { widget.editor.openTuneEditor(); }, ), CircularIconButton( svgPath: "assets/image-editor/image-editor-paint.svg", - label: "Draw", + label: S.of(context).draw, onTap: () { widget.editor.openPaintingEditor(); }, @@ -120,7 +121,7 @@ class ImageEditorMainBottomBarState extends State CircularIconButton( svgPath: "assets/image-editor/image-editor-sticker.svg", - label: "Sticker", + label: S.of(context).sticker, onTap: () { widget.editor.openEmojiEditor(); }, diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart index 0e4d1dd44f..5c7fe2c97d 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_paint_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart"; @@ -63,7 +64,7 @@ class _ImageEditorPaintBarState extends State Padding( padding: const EdgeInsets.only(left: 20.0), child: Text( - "Brush Color", + S.of(context).brushColor, style: getEnteTextTheme(context).body, ), ), diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart index 3115aa3933..d4327fb136 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_text_bar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import "package:flutter_svg/svg.dart"; +import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart"; import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart"; @@ -75,7 +76,7 @@ class _ImageEditorTextBarState extends State children: [ CircularIconButton( svgPath: "assets/image-editor/image-editor-text-color.svg", - label: "Color", + label: S.of(context).color, isSelected: selectedActionIndex == 0, onTap: () { _selectAction(0); @@ -83,7 +84,7 @@ class _ImageEditorTextBarState extends State ), CircularIconButton( svgPath: "assets/image-editor/image-editor-text-font.svg", - label: "Font", + label: S.of(context).font, isSelected: selectedActionIndex == 1, onTap: () { _selectAction(1); @@ -91,7 +92,7 @@ class _ImageEditorTextBarState extends State ), CircularIconButton( svgPath: "assets/image-editor/image-editor-text-background.svg", - label: "Background", + label: S.of(context).background, isSelected: selectedActionIndex == 2, onTap: () { setState(() { @@ -101,7 +102,7 @@ class _ImageEditorTextBarState extends State ), CircularIconButton( svgPath: "assets/image-editor/image-editor-text-align-left.svg", - label: "Align", + label: S.of(context).align, isSelected: selectedActionIndex == 3, onTap: () { setState(() { diff --git a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart index 5cb0d8fcbe..47ff3c11cc 100644 --- a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart +++ b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart @@ -20,7 +20,6 @@ import "package:photos/states/detail_page_state.dart"; import "package:photos/ui/common/fast_scroll_physics.dart"; import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart"; -import 'package:photos/ui/tools/editor/image_editor_page.dart'; import "package:photos/ui/tools/editor/video_editor_page.dart"; import "package:photos/ui/viewer/file/file_app_bar.dart"; import "package:photos/ui/viewer/file/file_bottom_bar.dart"; @@ -176,7 +175,7 @@ class _DetailPageState extends State { builder: (BuildContext context, int selectedIndex, _) { return FileBottomBar( _files![selectedIndex], - _onNewImageEditor, + _onEditFileRequested, widget.config.mode == DetailPageMode.minimalistic && !isGuestView, onFileRemoved: _onFileRemoved, @@ -358,7 +357,7 @@ class _DetailPageState extends State { } } - Future _onNewImageEditor(EnteFile file) async { + Future _onEditFileRequested(EnteFile file) async { if (file.uploadedFileID != null && file.ownerID != Configuration.instance.getUserID()) { _logger.severe( @@ -420,67 +419,6 @@ class _DetailPageState extends State { } } - Future _onEditFileRequested(EnteFile file) async { - if (file.uploadedFileID != null && - file.ownerID != Configuration.instance.getUserID()) { - _logger.severe( - "Attempt to edit unowned file", - UnauthorizedEditError(), - StackTrace.current, - ); - // ignore: unawaited_futures - showErrorDialog( - context, - S.of(context).sorry, - S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont, - ); - return; - } - final dialog = createProgressDialog(context, S.of(context).pleaseWait); - await dialog.show(); - try { - final ioFile = await getFile(file); - if (ioFile == null) { - showShortToast(context, S.of(context).failedToFetchOriginalForEdit); - await dialog.hide(); - return; - } - if (file.fileType == FileType.video) { - await dialog.hide(); - replacePage( - context, - VideoEditorPage( - file: file, - ioFile: ioFile, - detailPageConfig: widget.config.copyWith( - files: _files, - selectedIndex: _selectedIndexNotifier.value, - ), - ), - ); - return; - } - final imageProvider = - ExtendedFileImageProvider(ioFile, cacheRawData: true); - await precacheImage(imageProvider, context); - await dialog.hide(); - replacePage( - context, - ImageEditorPage( - imageProvider, - file, - widget.config.copyWith( - files: _files, - selectedIndex: _selectedIndexNotifier.value, - ), - ), - ); - } catch (e) { - await dialog.hide(); - _logger.warning("Failed to initiate edit", e); - } - } - Future _requestAuthentication() async { return await LocalAuthenticationService.instance.requestLocalAuthentication( context, From 557563e1b7d830bf5d2678882c9a28e881adcfd5 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 31 Jul 2025 15:38:58 +0530 Subject: [PATCH 30/57] Keep InheritedDetailPageState and DetailPage's body in different widgets to avoid InheritedDetailPageState from getting reinitialized and losing it's state when body of DetailPage rebuilds --- .../lib/ui/viewer/file/detail_page.dart | 196 ++++++++++-------- 1 file changed, 104 insertions(+), 92 deletions(-) diff --git a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart index 5cb0d8fcbe..42b0a71954 100644 --- a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart +++ b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart @@ -64,16 +64,30 @@ class DetailPageConfiguration { } } -class DetailPage extends StatefulWidget { +class DetailPage extends StatelessWidget { final DetailPageConfiguration config; const DetailPage(this.config, {super.key}); @override - State createState() => _DetailPageState(); + Widget build(BuildContext context) { + // Separating body to a different widget to avoid + // unnecessary reinitialization of the InheritedDetailPageState + // when the body is rebuilt, which can reset state stored in it. + return InheritedDetailPageState(child: _Body(config)); + } } -class _DetailPageState extends State { +class _Body extends StatefulWidget { + final DetailPageConfiguration config; + + const _Body(this.config); + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { final _logger = Logger("DetailPageState"); bool _shouldDisableScroll = false; List? _files; @@ -137,102 +151,100 @@ class _DetailPageState extends State { _files!.length.toString() + " files .", ); - return InheritedDetailPageState( - child: PopScope( - canPop: !isGuestView, - onPopInvokedWithResult: (didPop, _) async { - if (isGuestView) { - final authenticated = await _requestAuthentication(); - if (authenticated) { - Bus.instance.fire(GuestViewEvent(false, false)); - await localSettings.setOnGuestView(false); - } + return PopScope( + canPop: !isGuestView, + onPopInvokedWithResult: (didPop, _) async { + if (isGuestView) { + final authenticated = await _requestAuthentication(); + if (authenticated) { + Bus.instance.fire(GuestViewEvent(false, false)); + await localSettings.setOnGuestView(false); } - }, - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(80), - child: ValueListenableBuilder( - builder: (BuildContext context, int selectedIndex, _) { - return FileAppBar( - _files![selectedIndex], - _onFileRemoved, - widget.config.mode == DetailPageMode.full, - enableFullScreenNotifier: InheritedDetailPageState.of(context) - .enableFullScreenNotifier, - ); - }, - valueListenable: _selectedIndexNotifier, - ), + } + }, + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(80), + child: ValueListenableBuilder( + builder: (BuildContext context, int selectedIndex, _) { + return FileAppBar( + _files![selectedIndex], + _onFileRemoved, + widget.config.mode == DetailPageMode.full, + enableFullScreenNotifier: InheritedDetailPageState.of(context) + .enableFullScreenNotifier, + ); + }, + valueListenable: _selectedIndexNotifier, ), - extendBodyBehindAppBar: true, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.black, - body: Center( - child: Stack( - children: [ - _buildPageView(), - ValueListenableBuilder( - builder: (BuildContext context, int selectedIndex, _) { - return FileBottomBar( - _files![selectedIndex], - _onNewImageEditor, - widget.config.mode == DetailPageMode.minimalistic && - !isGuestView, - onFileRemoved: _onFileRemoved, - userID: Configuration.instance.getUserID(), - enableFullScreenNotifier: - InheritedDetailPageState.of(context) - .enableFullScreenNotifier, - ); - }, - valueListenable: _selectedIndexNotifier, - ), - ValueListenableBuilder( - valueListenable: _selectedIndexNotifier, - builder: (BuildContext context, int selectedIndex, _) { - if (_files![selectedIndex].isPanorama() == true) { - return ValueListenableBuilder( - valueListenable: InheritedDetailPageState.of(context) + ), + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, + body: Center( + child: Stack( + children: [ + _buildPageView(), + ValueListenableBuilder( + builder: (BuildContext context, int selectedIndex, _) { + return FileBottomBar( + _files![selectedIndex], + _onNewImageEditor, + widget.config.mode == DetailPageMode.minimalistic && + !isGuestView, + onFileRemoved: _onFileRemoved, + userID: Configuration.instance.getUserID(), + enableFullScreenNotifier: + InheritedDetailPageState.of(context) .enableFullScreenNotifier, - builder: (context, value, child) { - return IgnorePointer( - ignoring: value, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: !value ? 1.0 : 0.0, - child: Align( - alignment: Alignment.center, - child: Tooltip( - message: S.of(context).panorama, - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: const Color(0xAA252525), - fixedSize: const Size(44, 44), - ), - icon: const Icon( - Icons.threesixty, - color: Colors.white, - size: 26, - ), - onPressed: () async { - await openPanoramaViewerPage( - _files![selectedIndex], - ); - }, + ); + }, + valueListenable: _selectedIndexNotifier, + ), + ValueListenableBuilder( + valueListenable: _selectedIndexNotifier, + builder: (BuildContext context, int selectedIndex, _) { + if (_files![selectedIndex].isPanorama() == true) { + return ValueListenableBuilder( + valueListenable: InheritedDetailPageState.of(context) + .enableFullScreenNotifier, + builder: (context, value, child) { + return IgnorePointer( + ignoring: value, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: !value ? 1.0 : 0.0, + child: Align( + alignment: Alignment.center, + child: Tooltip( + message: S.of(context).panorama, + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: const Color(0xAA252525), + fixedSize: const Size(44, 44), ), + icon: const Icon( + Icons.threesixty, + color: Colors.white, + size: 26, + ), + onPressed: () async { + await openPanoramaViewerPage( + _files![selectedIndex], + ); + }, ), ), ), - ); - }, - ); - } - return const SizedBox(); - }, - ), - ], - ), + ), + ); + }, + ); + } + return const SizedBox(); + }, + ), + ], ), ), ), From 783d70a8f1a5930116ee5a9d1fbb1e4ca62a5168 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 31 Jul 2025 22:38:04 +0530 Subject: [PATCH 31/57] bump up build number --- mobile/apps/photos/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/pubspec.yaml b/mobile/apps/photos/pubspec.yaml index 8d118c27ee..16e9a28d65 100644 --- a/mobile/apps/photos/pubspec.yaml +++ b/mobile/apps/photos/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.0+1120 +version: 1.2.0+1200 publish_to: none environment: From 2fe3c61621bc2be71e4609e9fe24950e9b6297cc Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:34:53 +0530 Subject: [PATCH 32/57] [mob][photos] Upgrade media_kit --- mobile/apps/photos/ios/Podfile.lock | 133 +++++++++--------- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- mobile/apps/photos/pubspec.lock | 82 +++++------ 3 files changed, 114 insertions(+), 107 deletions(-) diff --git a/mobile/apps/photos/ios/Podfile.lock b/mobile/apps/photos/ios/Podfile.lock index 55224eaaf9..627d2debd6 100644 --- a/mobile/apps/photos/ios/Podfile.lock +++ b/mobile/apps/photos/ios/Podfile.lock @@ -12,6 +12,8 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - emoji_picker_flutter (0.0.1): + - Flutter - ffmpeg_kit_custom (6.0.3) - ffmpeg_kit_flutter (6.0.3): - ffmpeg_kit_custom @@ -127,9 +129,6 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - local_auth_darwin (0.0.1): - - Flutter - - FlutterMacOS - local_auth_ios (0.0.1): - Flutter - Mantle (2.2.0): @@ -230,6 +229,8 @@ PODS: - Flutter - url_launcher_ios (0.0.1): - Flutter + - vibration (1.7.5): + - Flutter - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS @@ -250,6 +251,7 @@ DEPENDENCIES: - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - ffmpeg_kit_flutter (from `.symlinks/plugins/ffmpeg_kit_flutter/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -269,7 +271,6 @@ DEPENDENCIES: - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`) - - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - maps_launcher (from `.symlinks/plugins/maps_launcher/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) @@ -297,6 +298,7 @@ DEPENDENCIES: - thermal (from `.symlinks/plugins/thermal/ios`) - ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - vibration (from `.symlinks/plugins/vibration/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) @@ -304,7 +306,7 @@ DEPENDENCIES: - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: - https://github.com/ente-io/ffmpeg-kit-custom-repo-ios: + https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git: - ffmpeg_kit_custom trunk: - Firebase @@ -339,6 +341,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/dart_ui_isolate/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" ffmpeg_kit_flutter: :path: ".symlinks/plugins/ffmpeg_kit_flutter/ios" file_saver: @@ -377,8 +381,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" launcher_icon_switcher: :path: ".symlinks/plugins/launcher_icon_switcher/ios" - local_auth_darwin: - :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" maps_launcher: @@ -433,6 +435,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/ua_client_hints/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + vibration: + :path: ".symlinks/plugins/vibration/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" video_thumbnail: @@ -445,83 +449,84 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 - battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd - cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c - dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6 + battery_info: b6c551049266af31556b93c9d9b9452cfec0219f + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba + dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58 ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99 - ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5 - file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf - firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f - firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac + firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682 + firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58 - flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987 - flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 - fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + flutter_sodium: a00383520fc689c688b66fd3092984174712493e + flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f - image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1 - in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da + home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 + image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 + in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45 - media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd - media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 - media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 - motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1 - motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1 - move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84 + maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203 + media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84 + media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 + media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e + motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91 + motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4 + move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d - objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 - onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2 + native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e + objective_c: 77e887b5ba1827970907e10e832eec1683f3431d + onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b - open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11 + open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 - privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 + receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1 SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 - sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 - sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832 - system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 - thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41 - ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b - video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620 - volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 - wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 - workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e + sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa + system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa + thermal: a9261044101ae8f532fa29cab4e8270b51b3f55c + ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40 + volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7 + wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18 diff --git a/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj b/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj index d1c1bff9f4..db6d40da7d 100644 --- a/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/apps/photos/ios/Runner.xcodeproj/project.pbxproj @@ -532,6 +532,7 @@ "${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework", "${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", + "${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework", "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", "${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework", "${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework", @@ -548,7 +549,6 @@ "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", - "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", "${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", @@ -576,6 +576,7 @@ "${BUILT_PRODUCTS_DIR}/thermal/thermal.framework", "${BUILT_PRODUCTS_DIR}/ua_client_hints/ua_client_hints.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", + "${BUILT_PRODUCTS_DIR}/vibration/vibration.framework", "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", "${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework", "${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework", @@ -628,6 +629,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework", @@ -644,7 +646,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", @@ -672,6 +673,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/thermal.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ua_client_hints.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/vibration.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework", diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index 13f6212d27..15724d09da 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,7 +21,7 @@ packages: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" adaptive_theme: dependency: "direct main" description: @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" android_intent_plus: dependency: "direct main" description: @@ -317,10 +317,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" computer: dependency: "direct main" description: @@ -1424,18 +1424,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1544,10 +1544,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" maps_launcher: dependency: "direct main" description: @@ -1586,24 +1586,24 @@ packages: description: path: media_kit ref: HEAD - resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80" + resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://github.com/media-kit/media-kit" source: git - version: "1.1.11" + version: "1.2.0" media_kit_libs_android_video: dependency: transitive description: name: media_kit_libs_android_video - sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c" + sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7 url: "https://pub.dev" source: hosted - version: "1.3.6" + version: "1.3.7" media_kit_libs_ios_video: dependency: "direct main" description: path: "libs/ios/media_kit_libs_ios_video" ref: HEAD - resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80" + resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://github.com/media-kit/media-kit" source: git version: "1.1.4" @@ -1611,10 +1611,10 @@ packages: dependency: transitive description: name: media_kit_libs_linux - sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.1" media_kit_libs_macos_video: dependency: transitive description: @@ -1628,27 +1628,27 @@ packages: description: path: "libs/universal/media_kit_libs_video" ref: HEAD - resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80" + resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://github.com/media-kit/media-kit" source: git - version: "1.0.5" + version: "1.0.6" media_kit_libs_windows_video: dependency: transitive description: name: media_kit_libs_windows_video - sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887" + sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab url: "https://pub.dev" source: hosted - version: "1.0.10" + version: "1.0.11" media_kit_video: dependency: "direct main" description: path: media_kit_video ref: HEAD - resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80" + resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://github.com/media-kit/media-kit" source: git - version: "1.2.5" + version: "1.3.0" meta: dependency: transitive description: @@ -2325,7 +2325,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -2450,10 +2450,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" step_progress_indicator: dependency: "direct main" description: @@ -2482,10 +2482,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" styled_text: dependency: "direct main" description: @@ -2546,26 +2546,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.7" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" thermal: dependency: "direct main" description: @@ -2845,10 +2845,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" volume_controller: dependency: transitive description: @@ -2909,10 +2909,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: From 5a9684f2517deb893e81016ae05b0ded1eecbf33 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:43:00 +0530 Subject: [PATCH 33/57] [mob][photos] Upgrade motionphoto (iOS) pkg --- mobile/apps/photos/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index 15724d09da..ec4f20df2a 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -1712,7 +1712,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5" + resolved-ref: "64e47a446bf3b64f012f2076481cebea51ca27cf" url: "https://github.com/ente-io/motionphoto.git" source: git version: "0.0.1" From 14873623664c662bea9210c3b415416adc18b052 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 1 Aug 2025 12:18:56 +0530 Subject: [PATCH 34/57] Remove old image editor --- .../ui/tools/editor/image_editor_page.dart | 553 ------------------ 1 file changed, 553 deletions(-) delete mode 100644 mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart deleted file mode 100644 index 99874d7c3a..0000000000 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart +++ /dev/null @@ -1,553 +0,0 @@ -import "dart:async"; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; -import 'dart:ui' as ui show Image; - -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/material.dart'; -import "package:flutter_image_compress/flutter_image_compress.dart"; -import 'package:image_editor/image_editor.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/db/files_db.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; -import "package:photos/generated/l10n.dart"; -import 'package:photos/models/file/file.dart' as ente; -import 'package:photos/models/location/location.dart'; -import 'package:photos/services/sync/sync_service.dart'; -import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/components/action_sheet_widget.dart'; -import 'package:photos/ui/components/buttons/button_widget.dart'; -import 'package:photos/ui/components/models/button_type.dart'; -import 'package:photos/ui/notification/toast.dart'; -import 'package:photos/ui/tools/editor/filtered_image.dart'; -import 'package:photos/ui/viewer/file/detail_page.dart'; -import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/navigation_util.dart'; -import 'package:syncfusion_flutter_core/theme.dart'; -import 'package:syncfusion_flutter_sliders/sliders.dart'; - -class ImageEditorPage extends StatefulWidget { - final ImageProvider imageProvider; - final DetailPageConfiguration detailPageConfig; - final ente.EnteFile originalFile; - - const ImageEditorPage( - this.imageProvider, - this.originalFile, - this.detailPageConfig, { - super.key, - }); - - @override - State createState() => _ImageEditorPageState(); -} - -class _ImageEditorPageState extends State { - static const double kBrightnessDefault = 1; - static const double kBrightnessMin = 0; - static const double kBrightnessMax = 2; - static const double kSaturationDefault = 1; - static const double kSaturationMin = 0; - static const double kSaturationMax = 2; - - final _logger = Logger("ImageEditor"); - final GlobalKey editorKey = - GlobalKey(); - - double? _brightness = kBrightnessDefault; - double? _saturation = kSaturationDefault; - bool _hasEdited = false; - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, _) async { - if (_hasBeenEdited()) { - await _showExitConfirmationDialog(context); - } else { - replacePage(context, DetailPage(widget.detailPageConfig)); - } - }, - child: Scaffold( - appBar: AppBar( - backgroundColor: const Color(0x00000000), - elevation: 0, - actions: _hasBeenEdited() - ? [ - IconButton( - padding: const EdgeInsets.only(right: 16, left: 16), - onPressed: () { - editorKey.currentState!.reset(); - setState(() { - _brightness = kBrightnessDefault; - _saturation = kSaturationDefault; - }); - }, - icon: const Icon(Icons.history), - ), - ] - : [], - ), - body: Column( - children: [ - Expanded(child: _buildImage()), - const Padding(padding: EdgeInsets.all(4)), - Column( - children: [ - _buildBrightness(), - _buildSat(), - ], - ), - const Padding(padding: EdgeInsets.all(8)), - SafeArea(child: _buildBottomBar()), - Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)), - ], - ), - ), - ); - } - - bool _hasBeenEdited() { - return _hasEdited || - _saturation != kSaturationDefault || - _brightness != kBrightnessDefault; - } - - Widget _buildImage() { - return Hero( - tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag, - child: ExtendedImage( - image: widget.imageProvider, - extendedImageEditorKey: editorKey, - mode: ExtendedImageMode.editor, - fit: BoxFit.contain, - initEditorConfigHandler: (_) => EditorConfig( - maxScale: 8.0, - cropRectPadding: const EdgeInsets.all(20.0), - hitTestSize: 20.0, - cornerColor: const Color.fromRGBO(45, 150, 98, 1), - editActionDetailsIsChanged: (_) { - setState(() { - _hasEdited = true; - }); - }, - ), - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.completed) { - return FilteredImage( - brightness: _brightness, - saturation: _saturation, - child: state.completedWidget, - ); - } - return const EnteLoadingWidget(); - }, - ), - ); - } - - Widget _buildBottomBar() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildFlipButton(), - _buildRotateLeftButton(), - _buildRotateRightButton(), - _buildSaveButton(), - ], - ); - } - - Widget _buildFlipButton() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - flip(); - }, - child: SizedBox( - width: 80, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Icon( - Icons.flip, - color: Theme.of(context).iconTheme.color!.withOpacity(0.8), - size: 20, - ), - ), - const Padding(padding: EdgeInsets.all(2)), - Text( - S.of(context).flip, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - Widget _buildRotateLeftButton() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - rotate(false); - }, - child: SizedBox( - width: 80, - child: Column( - children: [ - Icon( - Icons.rotate_left, - color: Theme.of(context).iconTheme.color!.withOpacity(0.8), - ), - const Padding(padding: EdgeInsets.all(2)), - Text( - S.of(context).rotateLeft, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - Widget _buildRotateRightButton() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - rotate(true); - }, - child: SizedBox( - width: 80, - child: Column( - children: [ - Icon( - Icons.rotate_right, - color: Theme.of(context).iconTheme.color!.withOpacity(0.8), - ), - const Padding(padding: EdgeInsets.all(2)), - Text( - S.of(context).rotateRight, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - Widget _buildSaveButton() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - _saveEdits(); - }, - child: SizedBox( - width: 80, - child: Column( - children: [ - Icon( - Icons.save_alt_outlined, - color: Theme.of(context).iconTheme.color!.withOpacity(0.8), - ), - const Padding(padding: EdgeInsets.all(2)), - Text( - S.of(context).saveCopy, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - Future _saveEdits() async { - final dialog = createProgressDialog(context, S.of(context).saving); - await dialog.show(); - final ExtendedImageEditorState? state = editorKey.currentState; - if (state == null) { - return; - } - final Rect? rect = state.getCropRect(); - if (rect == null) { - return; - } - final EditActionDetails action = state.editAction!; - final double radian = action.rotateAngle; - - final bool flipHorizontal = action.flipY; - final bool flipVertical = action.flipX; - final Uint8List img = state.rawImageData; - - // ignore: unnecessary_null_comparison - if (img == null) { - _logger.severe("null rawImageData"); - showToast(context, S.of(context).somethingWentWrong); - return; - } - - final ImageEditorOption option = ImageEditorOption(); - - option.addOption(ClipOption.fromRect(rect)); - option.addOption( - FlipOption(horizontal: flipHorizontal, vertical: flipVertical), - ); - if (action.hasRotateAngle) { - option.addOption(RotateOption(radian.toInt())); - } - - option.addOption(ColorOption.saturation(_saturation!)); - option.addOption(ColorOption.brightness(_brightness!)); - - option.outputFormat = const OutputFormat.jpeg(100); - - final DateTime start = DateTime.now(); - Uint8List? result = await ImageEditor.editImage( - image: img, - imageEditorOption: option, - ); - if (result == null) { - _logger.severe("null result"); - showToast(context, S.of(context).somethingWentWrong); - return; - } - _logger.info('Size before compression = ${result.length}'); - - final ui.Image decodedResult = await decodeImageFromList(result); - result = await FlutterImageCompress.compressWithList( - result, - minWidth: decodedResult.width, - minHeight: decodedResult.height, - ); - _logger.info('Size after compression = ${result.length}'); - final Duration diff = DateTime.now().difference(start); - _logger.info('image_editor time : $diff'); - - try { - final fileName = - path.basenameWithoutExtension(widget.originalFile.title!) + - "_edited_" + - DateTime.now().microsecondsSinceEpoch.toString() + - ".JPEG"; - //Disabling notifications for assets changing to insert the file into - //files db before triggering a sync. - await PhotoManager.stopChangeNotify(); - final AssetEntity newAsset = - await (PhotoManager.editor.saveImage(result, filename: fileName)); - final newFile = await ente.EnteFile.fromAsset( - widget.originalFile.deviceFolder ?? '', - newAsset, - ); - - newFile.creationTime = widget.originalFile.creationTime; - newFile.collectionID = widget.originalFile.collectionID; - newFile.location = widget.originalFile.location; - if (!newFile.hasLocation && widget.originalFile.localID != null) { - final assetEntity = await widget.originalFile.getAsset; - if (assetEntity != null) { - final latLong = await assetEntity.latlngAsync(); - newFile.location = Location( - latitude: latLong.latitude, - longitude: latLong.longitude, - ); - } - } - newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile); - Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave")); - unawaited(SyncService.instance.sync()); - showShortToast(context, S.of(context).editsSaved); - _logger.info("Original file " + widget.originalFile.toString()); - _logger.info("Saved edits to file " + newFile.toString()); - final files = widget.detailPageConfig.files; - - // the index could be -1 if the files fetched doesn't contain the newly - // edited files - int selectionIndex = - files.indexWhere((file) => file.generatedID == newFile.generatedID); - if (selectionIndex == -1) { - files.add(newFile); - selectionIndex = files.length - 1; - } - await dialog.hide(); - replacePage( - context, - DetailPage( - widget.detailPageConfig.copyWith( - files: files, - selectedIndex: min(selectionIndex, files.length - 1), - ), - ), - ); - } catch (e, s) { - await dialog.hide(); - showToast(context, S.of(context).oopsCouldNotSaveEdits); - _logger.severe(e, s); - } finally { - await PhotoManager.startChangeNotify(); - } - } - - void flip() { - editorKey.currentState?.flip(); - } - - void rotate(bool right) { - editorKey.currentState?.rotate(right: right); - } - - Widget _buildSat() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return Container( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: Row( - children: [ - SizedBox( - width: 42, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - S.of(context).color, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - ), - ), - ), - Expanded( - child: SfSliderTheme( - data: SfSliderThemeData( - activeTrackHeight: 4, - inactiveTrackHeight: 2, - inactiveTrackColor: Colors.grey[900], - activeTrackColor: const Color.fromRGBO(45, 150, 98, 1), - thumbColor: const Color.fromRGBO(45, 150, 98, 1), - thumbRadius: 10, - tooltipBackgroundColor: Colors.grey[900], - ), - child: SfSlider( - onChanged: (value) { - setState(() { - _saturation = value; - }); - }, - value: _saturation, - enableTooltip: true, - stepSize: 0.01, - min: kSaturationMin, - max: kSaturationMax, - ), - ), - ), - ], - ), - ); - } - - Widget _buildBrightness() { - final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!; - - return Container( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: Row( - children: [ - SizedBox( - width: 42, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - S.of(context).light, - style: subtitle2.copyWith( - color: subtitle2.color!.withOpacity(0.8), - ), - ), - ), - ), - Expanded( - child: SfSliderTheme( - data: SfSliderThemeData( - activeTrackHeight: 4, - inactiveTrackHeight: 2, - activeTrackColor: const Color.fromRGBO(45, 150, 98, 1), - inactiveTrackColor: Colors.grey[900], - thumbColor: const Color.fromRGBO(45, 150, 98, 1), - thumbRadius: 10, - tooltipBackgroundColor: Colors.grey[900], - ), - child: SfSlider( - onChanged: (value) { - setState(() { - _brightness = value; - }); - }, - value: _brightness, - enableTooltip: true, - stepSize: 0.01, - min: kBrightnessMin, - max: kBrightnessMax, - ), - ), - ), - ], - ), - ); - } - - Future _showExitConfirmationDialog(BuildContext context) async { - final actionResult = await showActionSheet( - context: context, - buttons: [ - ButtonWidget( - labelText: S.of(context).yesDiscardChanges, - buttonType: ButtonType.critical, - buttonSize: ButtonSize.large, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.first, - isInAlert: true, - ), - ButtonWidget( - labelText: S.of(context).no, - buttonType: ButtonType.secondary, - buttonSize: ButtonSize.large, - buttonAction: ButtonAction.second, - shouldStickToDarkTheme: true, - isInAlert: true, - ), - ], - body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade, - actionSheetType: ActionSheetType.defaultActionSheet, - ); - if (actionResult?.action != null && - actionResult!.action == ButtonAction.first) { - replacePage(context, DetailPage(widget.detailPageConfig)); - } - } -} From 7cd95e636980aeb757ac1cecf42c2201ae8299b9 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 1 Aug 2025 12:19:14 +0530 Subject: [PATCH 35/57] Minor refractor --- ...{image_editor_page_new.dart => image_editor_page.dart} | 8 ++++---- mobile/apps/photos/lib/ui/viewer/file/detail_page.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename mobile/apps/photos/lib/ui/tools/editor/image_editor/{image_editor_page_new.dart => image_editor_page.dart} (99%) diff --git a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart similarity index 99% rename from mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart rename to mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart index f99799378e..e6bc90dbab 100644 --- a/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page_new.dart +++ b/mobile/apps/photos/lib/ui/tools/editor/image_editor/image_editor_page.dart @@ -37,12 +37,12 @@ import "package:photos/utils/navigation_util.dart"; import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart"; import 'package:pro_image_editor/pro_image_editor.dart'; -class NewImageEditor extends StatefulWidget { +class ImageEditorPage extends StatefulWidget { final ente.EnteFile originalFile; final File file; final DetailPageConfiguration detailPageConfig; - const NewImageEditor({ + const ImageEditorPage({ super.key, required this.file, required this.originalFile, @@ -50,10 +50,10 @@ class NewImageEditor extends StatefulWidget { }); @override - State createState() => _NewImageEditorState(); + State createState() => _ImageEditorPageState(); } -class _NewImageEditorState extends State { +class _ImageEditorPageState extends State { final _mainEditorBarKey = GlobalKey(); final editorKey = GlobalKey(); final _logger = Logger("ImageEditor"); diff --git a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart index 47ff3c11cc..8f71ccc3df 100644 --- a/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart +++ b/mobile/apps/photos/lib/ui/viewer/file/detail_page.dart @@ -19,7 +19,7 @@ import "package:photos/services/local_authentication_service.dart"; import "package:photos/states/detail_page_state.dart"; import "package:photos/ui/common/fast_scroll_physics.dart"; import 'package:photos/ui/notification/toast.dart'; -import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart"; +import "package:photos/ui/tools/editor/image_editor/image_editor_page.dart"; import "package:photos/ui/tools/editor/video_editor_page.dart"; import "package:photos/ui/viewer/file/file_app_bar.dart"; import "package:photos/ui/viewer/file/file_bottom_bar.dart"; @@ -404,7 +404,7 @@ class _DetailPageState extends State { await dialog.hide(); replacePage( context, - NewImageEditor( + ImageEditorPage( originalFile: file, file: ioFile, detailPageConfig: widget.config.copyWith( From 3c8d8067c13d54790364d516a643b3128d149ffd Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 1 Aug 2025 12:19:46 +0530 Subject: [PATCH 36/57] Remove image_editor dependency from pubspec.yaml and pubspec.lock --- mobile/apps/photos/pubspec.lock | 36 ++------------------------------- mobile/apps/photos/pubspec.yaml | 1 - 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index 13f6212d27..121d0f8ae2 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -1271,38 +1271,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" - image_editor: - dependency: "direct main" - description: - name: image_editor - sha256: "38070067264fd9fea4328ca630d2ff7bd65ebe6aa4ed375d983b732d2ae7146b" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - image_editor_common: - dependency: transitive - description: - name: image_editor_common - sha256: "93d2f5c8b636f862775dd62a9ec20d09c8272598daa02f935955a4640e1844ee" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - image_editor_ohos: - dependency: transitive - description: - name: image_editor_ohos - sha256: "06756859586d5acefec6e3b4f356f9b1ce05ef09213bcb9a0ce1680ecea2d054" - url: "https://pub.dev" - source: hosted - version: "0.0.9" - image_editor_platform_interface: - dependency: transitive - description: - name: image_editor_platform_interface - sha256: "474517efc770464f7d99942472d8cfb369a3c378e95466ec17f74d2b80bd40de" - url: "https://pub.dev" - source: hosted - version: "1.1.0" in_app_purchase: dependency: "direct main" description: @@ -2845,10 +2813,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" volume_controller: dependency: transitive description: diff --git a/mobile/apps/photos/pubspec.yaml b/mobile/apps/photos/pubspec.yaml index 8d118c27ee..830fa50bc9 100644 --- a/mobile/apps/photos/pubspec.yaml +++ b/mobile/apps/photos/pubspec.yaml @@ -114,7 +114,6 @@ dependencies: html_unescape: ^2.0.0 http: ^1.1.0 image: ^4.0.17 - image_editor: ^1.6.0 in_app_purchase: ^3.0.7 intl: ^0.19.0 latlong2: ^0.9.0 From ecf236ad54b2a3610336c18cdab057b4ee81692c Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 1 Aug 2025 12:21:08 +0530 Subject: [PATCH 37/57] Add localization strings for editing tools and remove filtered_image.dart --- mobile/apps/photos/lib/generated/l10n.dart | 112 ++++++++++++++++++ .../lib/ui/tools/editor/filtered_image.dart | 110 ----------------- 2 files changed, 112 insertions(+), 110 deletions(-) delete mode 100644 mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart diff --git a/mobile/apps/photos/lib/generated/l10n.dart b/mobile/apps/photos/lib/generated/l10n.dart index 84cef71699..14b4c3aeea 100644 --- a/mobile/apps/photos/lib/generated/l10n.dart +++ b/mobile/apps/photos/lib/generated/l10n.dart @@ -12495,6 +12495,118 @@ class S { args: [], ); } + + /// `Undo` + String get undo { + return Intl.message( + 'Undo', + name: 'undo', + desc: '', + args: [], + ); + } + + /// `Redo` + String get redo { + return Intl.message( + 'Redo', + name: 'redo', + desc: '', + args: [], + ); + } + + /// `Filter` + String get filter { + return Intl.message( + 'Filter', + name: 'filter', + desc: '', + args: [], + ); + } + + /// `Adjust` + String get adjust { + return Intl.message( + 'Adjust', + name: 'adjust', + desc: '', + args: [], + ); + } + + /// `Draw` + String get draw { + return Intl.message( + 'Draw', + name: 'draw', + desc: '', + args: [], + ); + } + + /// `Sticker` + String get sticker { + return Intl.message( + 'Sticker', + name: 'sticker', + desc: '', + args: [], + ); + } + + /// `Brush Color` + String get brushColor { + return Intl.message( + 'Brush Color', + name: 'brushColor', + desc: '', + args: [], + ); + } + + /// `Font` + String get font { + return Intl.message( + 'Font', + name: 'font', + desc: '', + args: [], + ); + } + + /// `Background` + String get background { + return Intl.message( + 'Background', + name: 'background', + desc: '', + args: [], + ); + } + + /// `Align` + String get align { + return Intl.message( + 'Align', + name: 'align', + desc: '', + args: [], + ); + } + + /// `{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}` + String addedToAlbums(int count) { + return Intl.plural( + count, + one: 'Added successfully to 1 album', + other: 'Added successfully to $count albums', + name: 'addedToAlbums', + desc: 'Message shown when items are added to albums', + args: [count], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart b/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart deleted file mode 100644 index 31c74590ac..0000000000 --- a/mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/widgets.dart'; -import 'package:image_editor/image_editor.dart'; - -class FilteredImage extends StatelessWidget { - const FilteredImage({ - required this.child, - this.brightness, - this.saturation, - this.hue, - super.key, - }); - - final double? brightness, saturation, hue; - final Widget child; - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix( - ColorFilterGenerator.brightnessAdjustMatrix( - value: brightness ?? 1, - ), - ), - child: ColorFiltered( - colorFilter: ColorFilter.matrix( - ColorFilterGenerator.saturationAdjustMatrix( - value: saturation ?? 1, - ), - ), - child: ColorFiltered( - colorFilter: ColorFilter.matrix( - ColorFilterGenerator.hueAdjustMatrix( - value: hue ?? 0, - ), - ), - child: child, - ), - ), - ); - } -} - -class ColorFilterGenerator { - static List hueAdjustMatrix({double value = 1}) { - value = value * pi; - - if (value == 0) { - return [ - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]; - } - final double cosVal = cos(value); - final double sinVal = sin(value); - const double lumR = 0.213; - const double lumG = 0.715; - const double lumB = 0.072; - - return List.from([ - (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), - (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), - (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), - 0, - 0, - (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), - (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), - (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), - 0, - 0, - (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), - (lumG + (cosVal * (-lumG))) + (sinVal * lumG), - (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]).map((i) => i.toDouble()).toList(); - } - - static List brightnessAdjustMatrix({double value = 1}) { - return ColorOption.brightness(value).matrix; - } - - static List saturationAdjustMatrix({double value = 1}) { - return ColorOption.saturation(value).matrix; - } -} From 0174d82829cdd4ff47c391f12df9b309d374a760 Mon Sep 17 00:00:00 2001 From: ian Date: Sat, 2 Aug 2025 00:02:32 +0800 Subject: [PATCH 38/57] Update README.md --- mobile/apps/photos/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/apps/photos/README.md b/mobile/apps/photos/README.md index 958f7c4215..45d096a77d 100644 --- a/mobile/apps/photos/README.md +++ b/mobile/apps/photos/README.md @@ -10,12 +10,12 @@ commit](https://github.com/ente-io/ente/commit/a8cdc811fd20ca4289d8e779c97f08ef5 Hello world -To know more about Ente, see [our main README](../README.md) or visit +To know more about Ente, see [our main README](../../../README.md) or visit [ente.io](https://ente.io). -To use Ente Photos on the web, see [../web](../web/README.md). To use Ente -Photos on the desktop, see [../desktop](../desktop/README.md). There is a also a -[CLI tool](../cli/README.md) for easy / automated exports. +To use Ente Photos on the web, see [../../../web](../../../web/README.md). To use Ente +Photos on the desktop, see [../../../desktop](../../../desktop/README.md). There is a also a +[CLI tool](../../../cli/README.md) for easy / automated exports. If you're looking for Ente Auth instead, see [../auth](../auth/README.md). @@ -32,16 +32,16 @@ without relying on third party stores. You can alternatively install the build from PlayStore or F-Droid. - + - + ### iOS - + ## 🧑‍💻 Building from source @@ -99,4 +99,4 @@ apksigner verify --print-certs ## 💚 Contribute -For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md). +For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md). From a4b938b5d505d01ea3f80b8b35d4dfa0af0d2a07 Mon Sep 17 00:00:00 2001 From: ian Date: Sat, 2 Aug 2025 00:07:37 +0800 Subject: [PATCH 39/57] Update README.md --- mobile/apps/auth/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/auth/README.md b/mobile/apps/auth/README.md index 99ad0d1688..db0c84ea26 100644 --- a/mobile/apps/auth/README.md +++ b/mobile/apps/auth/README.md @@ -98,7 +98,7 @@ more, see [docs/adding-icons](docs/adding-icons.md). The best way to support this project is by checking out [Ente Photos](../mobile/README.md) or spreading the word. -For more ways to contribute, see [../CONTRIBUTING.md](../../../CONTRIBUTING.md). +For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md). ## Certificate Fingerprints From b7ff0ca985af90f0b0b5992358426fe8b5db980e Mon Sep 17 00:00:00 2001 From: Rafael Ieda Date: Sat, 2 Aug 2025 10:27:27 -0300 Subject: [PATCH 40/57] updated ubiquiti auth custom icon --- mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg b/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg index 992eb7986a..559fd81869 100644 --- a/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg +++ b/mobile/apps/auth/assets/custom-icons/icons/ubiquiti.svg @@ -1,7 +1 @@ - - \ No newline at end of file + \ No newline at end of file From 658ba49186478eb4bb96432693075c85b89e1f56 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:42:27 +0530 Subject: [PATCH 41/57] [mob][photos] Log info about lock --- mobile/apps/photos/lib/db/upload_locks_db.dart | 17 +++++++++++++++++ mobile/apps/photos/lib/utils/file_uploader.dart | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/db/upload_locks_db.dart b/mobile/apps/photos/lib/db/upload_locks_db.dart index ab786d5c78..dc2bfab079 100644 --- a/mobile/apps/photos/lib/db/upload_locks_db.dart +++ b/mobile/apps/photos/lib/db/upload_locks_db.dart @@ -157,6 +157,23 @@ class UploadLocksDB { ); } + Future getLockData(String id) async { + final db = await instance.database; + final rows = await db.query( + _uploadLocksTable.table, + where: '${_uploadLocksTable.columnID} = ?', + whereArgs: [id], + ); + if (rows.isEmpty) { + return "No lock found for $id"; + } + final row = rows.first; + final time = row[_uploadLocksTable.columnTime] as int; + final owner = row[_uploadLocksTable.columnOwner] as String; + final duration = DateTime.now().millisecondsSinceEpoch - time; + return "Lock for $id acquired by $owner since ${Duration(milliseconds: duration)}"; + } + Future isLocked(String id, String owner) async { final db = await instance.database; final rows = await db.query( diff --git a/mobile/apps/photos/lib/utils/file_uploader.dart b/mobile/apps/photos/lib/utils/file_uploader.dart index 9bb28eeee4..890b167fea 100644 --- a/mobile/apps/photos/lib/utils/file_uploader.dart +++ b/mobile/apps/photos/lib/utils/file_uploader.dart @@ -519,7 +519,8 @@ class FileUploader { DateTime.now().microsecondsSinceEpoch, ); } catch (e) { - _logger.warning("Lock was already taken for " + file.toString()); + final lockInfo = await _uploadLocks.getLockData(lockKey); + _logger.warning("Lock was already taken ($lockInfo) for " + file.tag); throw LockAlreadyAcquiredError(); } From 568c5393a8d84eda1e874941ec2bbc0142cc4c21 Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:25:36 -0400 Subject: [PATCH 42/57] Fix broken link to adding-icons.md in CONTRIBUTING.md The link to the adding-icons.md file was broken, so I fixed it. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4b90454e2..83203d474e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents. ## Code contributions -If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug. +If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug. Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for. From 0d139df6520d715e44ce86f0b4989baae05dd68b Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:30:14 -0400 Subject: [PATCH 43/57] Added Smogon icon --- .../auth/assets/custom-icons/icons/Smogon.svg | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/Smogon.svg diff --git a/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg b/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg new file mode 100644 index 0000000000..6d4d656c88 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg @@ -0,0 +1,170 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e95aa5533905b323574cf2268551ee63c4509b6d Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:32:06 -0400 Subject: [PATCH 44/57] Added Smogon icon (filename fixed) The previous file name was "Smogon.svg." This time it's all lowercase. --- .../auth/assets/custom-icons/icons/smogon.svg | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/smogon.svg diff --git a/mobile/apps/auth/assets/custom-icons/icons/smogon.svg b/mobile/apps/auth/assets/custom-icons/icons/smogon.svg new file mode 100644 index 0000000000..6d4d656c88 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/smogon.svg @@ -0,0 +1,170 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 8d484528e76acc1b3586296c20eb59cff5647aab Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:32:50 -0400 Subject: [PATCH 45/57] Delete mobile/apps/auth/assets/custom-icons/icons/Smogon.svg --- .../auth/assets/custom-icons/icons/Smogon.svg | 170 ------------------ 1 file changed, 170 deletions(-) delete mode 100644 mobile/apps/auth/assets/custom-icons/icons/Smogon.svg diff --git a/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg b/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg deleted file mode 100644 index 6d4d656c88..0000000000 --- a/mobile/apps/auth/assets/custom-icons/icons/Smogon.svg +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From f1f84af3a7cc147b5aba991813be26f46184e826 Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:40:27 -0400 Subject: [PATCH 46/57] Add Smogon to custom-icons.json --- mobile/apps/auth/assets/custom-icons/_data/custom-icons.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index fc558cf8f5..a98501c3e9 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1505,6 +1505,9 @@ { "title": "Skinport" }, + { + "title": "Smogon" + }, { "title": "SMSPool", "slug": "sms_pool_net", @@ -1908,4 +1911,4 @@ "slug": "cowheels" } ] -} \ No newline at end of file +} From 824b071af4a0d5bf849a53ff4e9ac66d380d673b Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:28:45 -0400 Subject: [PATCH 47/57] Added more icons Icons for Twitter (before rebrand), Art Fight, Toyhouse, Animal Crossing, Best Buy, and Chucklefish. I wasn't able to find an official SVG of Toyhouse's icon or get it through inspect element, but I got it from here: https://logos.fandom.com/wiki/Toyhouse Animal Crossing Leaf: Nintendo, Public domain, via Wikimedia Commons Original Twitter logo: Martin Grasser, per source, Apache License 2.0 , via Wikimedia Commons Best Buy: Best Buy, Public domain, via Wikimedia Commons Chucklefish: https://en.wikipedia.org/wiki/File:Chucklefish.svg (Warns that this is not a free logo) --- .../custom-icons/icons/animal_crossing.svg | 1 + .../assets/custom-icons/icons/art_fight.svg | 307 ++++++++++++++++++ .../assets/custom-icons/icons/best_buy.svg | 13 + .../assets/custom-icons/icons/chucklefish.svg | 75 +++++ .../assets/custom-icons/icons/toyhouse.svg | 49 +++ .../assets/custom-icons/icons/twitter.svg | 4 + 6 files changed, 449 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/art_fight.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/best_buy.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/twitter.svg diff --git a/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg b/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg new file mode 100644 index 0000000000..6800a4d8f7 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/animal_crossing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg b/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg new file mode 100644 index 0000000000..51c4274bd4 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/art_fight.svg @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg b/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg new file mode 100644 index 0000000000..86717212e2 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/best_buy.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg b/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg new file mode 100644 index 0000000000..76be26a658 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/chucklefish.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg b/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg new file mode 100644 index 0000000000..f385ffbb27 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/toyhouse.svg @@ -0,0 +1,49 @@ + + + + diff --git a/mobile/apps/auth/assets/custom-icons/icons/twitter.svg b/mobile/apps/auth/assets/custom-icons/icons/twitter.svg new file mode 100644 index 0000000000..d60af2b8c5 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/twitter.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 12648ce726ea26706d8ab3fc99c29aa05e6a159f Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:00:23 -0400 Subject: [PATCH 48/57] Update custom-icons.json --- .../custom-icons/_data/custom-icons.json | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index a98501c3e9..9301d65a2c 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -60,6 +60,15 @@ "slug": "amtrak", "hex": "003A5D" }, + { + "title": "Animal Crossing", + "slug:": "animal_crossing", + "altNames": [ + "AnimalCrossing", + "Bell Tree Forums", + "BellTree Forums" + ] + }, { "title": "Ankama", "slug": "ankama" @@ -81,6 +90,13 @@ "Docaposte AR24" ] }, + { + "title": "Art Fight", + "slug": "art_fight", + "altNames": [ + "ArtFight" + ] + }, { "title": "Aruba", "slug": "aruba", @@ -341,6 +357,9 @@ "slug": "cih", "hex": "D14633" }, + { + "title": "Chucklefish" + }, { "title": "Clipper", "slug": "clippercard", @@ -1667,6 +1686,12 @@ { "title": "TorGuard" }, + { + "title": "Toyhouse", + "altNames": [ + "Toyhou.se" + ] + }, { "title": "Trading 212" }, @@ -1702,6 +1727,12 @@ "Twitch tv" ] }, + { + "title": "Twitter", + "altNames": [ + "X", + ] + }, { "title": "Ubiquiti", "slug": "ubiquiti", From 11786057e257e3786059bb1efcda9023a9a343cc Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:10:34 -0400 Subject: [PATCH 49/57] Moving broken link fix to another pull request --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83203d474e..f4b90454e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents. ## Code contributions -If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug. +If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug. Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for. From 047c2954f88c7ec7b451844bfcf6f4f55f4538a5 Mon Sep 17 00:00:00 2001 From: NylaTheWolf <41797151+NylaTheWolf@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:25:11 -0400 Subject: [PATCH 50/57] Actually fix broken link in CONTRIBUTING.md It's easier to just put everything in one pull request. The link to the adding-icons.md file was broken, so I fixed it. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4b90454e2..83203d474e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents. ## Code contributions -If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug. +If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug. Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for. From d7fdca78f75673edc2c3a8a195f14ad796b4364c Mon Sep 17 00:00:00 2001 From: Neeraj Date: Tue, 5 Aug 2025 10:20:49 +0530 Subject: [PATCH 51/57] Update pubspec.yaml --- mobile/apps/photos/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/pubspec.yaml b/mobile/apps/photos/pubspec.yaml index 16e9a28d65..2a47149f9b 100644 --- a/mobile/apps/photos/pubspec.yaml +++ b/mobile/apps/photos/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.0+1200 +version: 1.2.0+1201 publish_to: none environment: From 97d66a3afa4a0ff1f765b1051e4d9df35b2871b2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 5 Aug 2025 13:18:45 +0530 Subject: [PATCH 52/57] [rust] Make CI fail on warnings https://doc.rust-lang.org/stable/clippy/continuous_integration/github_actions.html --- .github/workflows/rust-lint.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-lint.yml b/.github/workflows/rust-lint.yml index 0f7694e0c6..802482c548 100644 --- a/.github/workflows/rust-lint.yml +++ b/.github/workflows/rust-lint.yml @@ -15,6 +15,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + RUSTFLAGS: -D warnings + jobs: lint: runs-on: ubuntu-latest @@ -33,9 +36,9 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - + - run: cargo fmt --check - - run: cargo clippy + - run: cargo clippy --all-targets --all-features - run: cargo build From 1648f62da64bb2b3e41a07fcad841cc7ed625c21 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Tue, 5 Aug 2025 15:23:17 +0530 Subject: [PATCH 53/57] Add repaint boundary over each gird item in gallery --- .../lib/models/gallery/gallery_groups.dart | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/mobile/apps/photos/lib/models/gallery/gallery_groups.dart b/mobile/apps/photos/lib/models/gallery/gallery_groups.dart index a42ba360e0..3bb9e70c1d 100644 --- a/mobile/apps/photos/lib/models/gallery/gallery_groups.dart +++ b/mobile/apps/photos/lib/models/gallery/gallery_groups.dart @@ -224,18 +224,20 @@ class GalleryGroups { int i = 0; while (!endOfListReached) { gridRowChildren.add( - GalleryFileWidget( + RepaintBoundary( key: ValueKey( tagPrefix + filesInGroup[firstIndexOfRowWrtFilesInGroup + i] .tag, ), - file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i], - selectedFiles: selectedFiles, - limitSelectionToOne: limitSelectionToOne, - tag: tagPrefix, - photoGridSize: crossAxisCount, - currentUserID: currentUserID, + child: GalleryFileWidget( + file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i], + selectedFiles: selectedFiles, + limitSelectionToOne: limitSelectionToOne, + tag: tagPrefix, + photoGridSize: crossAxisCount, + currentUserID: currentUserID, + ), ), ); @@ -247,18 +249,20 @@ class GalleryGroups { } else { for (int i = 0; i < crossAxisCount; i++) { gridRowChildren.add( - GalleryFileWidget( + RepaintBoundary( key: ValueKey( tagPrefix + filesInGroup[firstIndexOfRowWrtFilesInGroup + i] .tag, ), - file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i], - selectedFiles: selectedFiles, - limitSelectionToOne: limitSelectionToOne, - tag: tagPrefix, - photoGridSize: crossAxisCount, - currentUserID: currentUserID, + child: GalleryFileWidget( + file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i], + selectedFiles: selectedFiles, + limitSelectionToOne: limitSelectionToOne, + tag: tagPrefix, + photoGridSize: crossAxisCount, + currentUserID: currentUserID, + ), ), ); } From abe5548202eb41d82f9f16c037ab1f47854679fd Mon Sep 17 00:00:00 2001 From: peterv99 <145142820+peterv99@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:22:21 +0200 Subject: [PATCH 54/57] Added logos for meesman.nl, isc2.org, scouting.nl, zivver.com Fixed linting error in custom_icons.json (line 1733, superfluous comma) and added the aforementioned logos. --- .../custom-icons/_data/custom-icons.json | 18 ++- .../auth/assets/custom-icons/icons/isc2.svg | 4 + .../assets/custom-icons/icons/meesman.svg | 1 + .../custom-icons/icons/scoutingnederland.svg | 129 ++++++++++++++++++ .../auth/assets/custom-icons/icons/zivver.svg | 21 +++ 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/isc2.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/meesman.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/zivver.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index 9301d65a2c..d24b2ef968 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1730,7 +1730,7 @@ { "title": "Twitter", "altNames": [ - "X", + "X" ] }, { @@ -1940,6 +1940,22 @@ { "title": "Co-Wheels", "slug": "cowheels" + }, + { + "title": "Zivver", + "slug": "zivver" + }, + { + "title": "Meesman Indexbeleggen", + "slug": "meesman" + }, + { + "title": "Scouting Nederland", + "slug": "scoutingnederland" + }, + { + "title": "ISC2", + "slug": "isc2" } ] } diff --git a/mobile/apps/auth/assets/custom-icons/icons/isc2.svg b/mobile/apps/auth/assets/custom-icons/icons/isc2.svg new file mode 100644 index 0000000000..477c61750f --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/isc2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile/apps/auth/assets/custom-icons/icons/meesman.svg b/mobile/apps/auth/assets/custom-icons/icons/meesman.svg new file mode 100644 index 0000000000..7c98ba0161 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/meesman.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg b/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg new file mode 100644 index 0000000000..ae617a6aee --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/scoutingnederland.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/apps/auth/assets/custom-icons/icons/zivver.svg b/mobile/apps/auth/assets/custom-icons/icons/zivver.svg new file mode 100644 index 0000000000..40d0977f98 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/zivver.svg @@ -0,0 +1,21 @@ + + + + + + + + From 4f224e7eba4782bf7d12661db687bbf2e6800e5f Mon Sep 17 00:00:00 2001 From: eYdr1en <54525514+eYdr1en@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:10:42 +0200 Subject: [PATCH 55/57] add monochrome icon style for macos tray --- .../auth/assets/icons/auth-icon-monochrome.png | Bin 0 -> 29214 bytes mobile/apps/auth/lib/main.dart | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 mobile/apps/auth/assets/icons/auth-icon-monochrome.png diff --git a/mobile/apps/auth/assets/icons/auth-icon-monochrome.png b/mobile/apps/auth/assets/icons/auth-icon-monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..09deb41b4c08f80cefd1fc142c6f1367a843ff3a GIT binary patch literal 29214 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 initSystemTray() async { if (PlatformUtil.isMobile()) return; String path = Platform.isWindows ? 'assets/icons/auth-icon.ico' - : 'assets/icons/auth-icon.png'; + : Platform.isMacOS + ? 'assets/icons/auth-icon-monochrome.png' + : 'assets/icons/auth-icon.png'; await trayManager.setIcon(path); Menu menu = Menu( items: [ From 95347022e8d66a732e242164f99ee4aa274adcde Mon Sep 17 00:00:00 2001 From: eYdr1en <54525514+eYdr1en@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:20:44 +0200 Subject: [PATCH 56/57] add isTemplate for correct macos look on wallpaper --- mobile/apps/auth/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/auth/lib/main.dart b/mobile/apps/auth/lib/main.dart index c678ab4c51..2bca2feb0d 100644 --- a/mobile/apps/auth/lib/main.dart +++ b/mobile/apps/auth/lib/main.dart @@ -43,7 +43,7 @@ Future initSystemTray() async { : Platform.isMacOS ? 'assets/icons/auth-icon-monochrome.png' : 'assets/icons/auth-icon.png'; - await trayManager.setIcon(path); + await trayManager.setIcon(path, isTemplate: true); Menu menu = Menu( items: [ MenuItem( From 83395641efc0490b38843c836e47e92a32fe2d4d Mon Sep 17 00:00:00 2001 From: Muhammad Talal Anwar <3526562+talal@users.noreply.github.com> Date: Thu, 7 Aug 2025 04:49:40 +0000 Subject: [PATCH 57/57] chore: add TU Dresden icon --- .../auth/assets/custom-icons/_data/custom-icons.json | 9 +++++++++ .../apps/auth/assets/custom-icons/icons/tu_dresden.svg | 1 + 2 files changed, 10 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index 9301d65a2c..0524a34ab0 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1713,6 +1713,15 @@ "T Rowe Price Group, Inc" ] }, + { + "title": "TU Dresden", + "slug": "tu_dresden", + "altNames": [ + "Technische Universität Dresden", + "Dresden University of Technology" + ], + "hex": "00305d" + }, { "title": "Tweakers" }, diff --git a/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg b/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg new file mode 100644 index 0000000000..2c53b913cf --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/tu_dresden.svg @@ -0,0 +1 @@ + \ No newline at end of file