Files
ente/server/pkg/controller/public/file_link.go
2025-07-18 13:04:21 +05:30

153 lines
5.6 KiB
Go

package public
import (
"github.com/ente-io/museum/ente"
"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"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
)
// FileLinkController controls share collection operations
type FileLinkController struct {
FileController *controller.FileController
FileLinkRepo *public.FileLinkRepository
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, 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")
}
return c.mapRowToFileUrl(ctx, row), nil
}
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
// 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
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,
}
}