395 lines
11 KiB
Go
395 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/ente-io/museum/pkg/controller/file_copy"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ente-io/stacktrace"
|
|
"github.com/gin-contrib/requestid"
|
|
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"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// FileHandler exposes request handlers for all encrypted file related requests
|
|
type FileHandler struct {
|
|
Controller *controller.FileController
|
|
FileCopyCtrl *file_copy.FileCopyController
|
|
}
|
|
|
|
// DefaultMaxBatchSize is the default maximum API batch size unless specified otherwise
|
|
const DefaultMaxBatchSize = 1000
|
|
const DefaultCopyBatchSize = 100
|
|
|
|
// CreateOrUpdate creates an entry for a file
|
|
func (h *FileHandler) CreateOrUpdate(c *gin.Context) {
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
var file ente.File
|
|
if err := c.ShouldBindJSON(&file); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
file.UpdationTime = time.Microseconds()
|
|
|
|
// get an ente.App from the ?app= query parameter with a default of photos
|
|
enteApp := auth.GetApp(c)
|
|
|
|
if file.ID == 0 {
|
|
file.OwnerID = userID
|
|
file.IsDeleted = false
|
|
file, err := h.Controller.Create(c, userID, file, c.Request.UserAgent(), enteApp)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, file)
|
|
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)
|
|
}
|
|
|
|
// CopyFiles copies files that are owned by another user
|
|
func (h *FileHandler) CopyFiles(c *gin.Context) {
|
|
var req ente.CopyFileSyncRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
if len(req.CollectionFileItems) > DefaultCopyBatchSize {
|
|
handler.Error(c, stacktrace.Propagate(ente.NewBadRequestWithMessage(fmt.Sprintf("more than %d items", DefaultCopyBatchSize)), ""))
|
|
return
|
|
}
|
|
response, err := h.FileCopyCtrl.CopyFiles(c, req)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// Update updates already existing file
|
|
func (h *FileHandler) Update(c *gin.Context) {
|
|
enteApp := auth.GetApp(c)
|
|
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
var file ente.File
|
|
if err := c.ShouldBindJSON(&file); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
file.UpdationTime = time.Microseconds()
|
|
if file.ID <= 0 {
|
|
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "fileID should be >0"))
|
|
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)
|
|
}
|
|
|
|
// GetUploadURLs returns a bunch of urls where in the user can upload objects
|
|
func (h *FileHandler) GetUploadURLs(c *gin.Context) {
|
|
enteApp := auth.GetApp(c)
|
|
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
count, _ := strconv.Atoi(c.Query("count"))
|
|
urls, err := h.Controller.GetUploadURLs(c, userID, count, enteApp, false)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"urls": urls,
|
|
})
|
|
}
|
|
|
|
// GetMultipartUploadURLs returns an array of PartUpload PresignedURLs
|
|
func (h *FileHandler) GetMultipartUploadURLs(c *gin.Context) {
|
|
enteApp := auth.GetApp(c)
|
|
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
count, _ := strconv.Atoi(c.Query("count"))
|
|
urls, err := h.Controller.GetMultipartUploadURLs(c, userID, count, enteApp)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"urls": urls,
|
|
})
|
|
}
|
|
|
|
// Get redirects the request to the file location
|
|
func (h *FileHandler) Get(c *gin.Context) {
|
|
userID, fileID := getUserAndFileIDs(c)
|
|
url, err := h.Controller.GetFileURL(userID, fileID)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
h.logBadRedirect(c)
|
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
|
}
|
|
|
|
// GetV2 returns the URL of the file to client
|
|
func (h *FileHandler) GetV2(c *gin.Context) {
|
|
userID, fileID := getUserAndFileIDs(c)
|
|
url, err := h.Controller.GetFileURL(userID, fileID)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": url,
|
|
})
|
|
}
|
|
|
|
// GetThumbnail redirects the request to the file's thumbnail location
|
|
func (h *FileHandler) GetThumbnail(c *gin.Context) {
|
|
userID, fileID := getUserAndFileIDs(c)
|
|
url, err := h.Controller.GetThumbnailURL(userID, fileID)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
h.logBadRedirect(c)
|
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
|
}
|
|
|
|
// GetThumbnailV2 returns the URL of the thumbnail to the client
|
|
func (h *FileHandler) GetThumbnailV2(c *gin.Context) {
|
|
userID, fileID := getUserAndFileIDs(c)
|
|
url, err := h.Controller.GetThumbnailURL(userID, fileID)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": url,
|
|
})
|
|
}
|
|
|
|
// Trash moves the given files to the trash bin
|
|
func (h *FileHandler) Trash(c *gin.Context) {
|
|
var request ente.TrashRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, "failed to bind"))
|
|
return
|
|
}
|
|
if len(request.TrashItems) > DefaultMaxBatchSize {
|
|
handler.Error(c, stacktrace.Propagate(ente.ErrBatchSizeTooLarge, ""))
|
|
return
|
|
}
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
request.OwnerID = userID
|
|
err := h.Controller.Trash(c, userID, request)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
} else {
|
|
c.Status(http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// GetSize returns the size of files indicated by fileIDs
|
|
func (h *FileHandler) GetSize(c *gin.Context) {
|
|
var request ente.FileIDsRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
shouldReject, err := shouldRejectRequest(c)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
if shouldReject {
|
|
c.Status(http.StatusUpgradeRequired)
|
|
return
|
|
}
|
|
|
|
size, err := h.Controller.GetSize(userID, request.FileIDs)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
} else {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"size": size,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetInfo returns the FileInfo of files indicated by fileIDs
|
|
func (h *FileHandler) GetInfo(c *gin.Context) {
|
|
var request ente.FileIDsRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, "failed to bind request"))
|
|
return
|
|
}
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
|
|
response, err := h.Controller.GetFileInfo(c, userID, request.FileIDs)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
} else {
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
}
|
|
|
|
// shouldRejectRequest return true if the client which is making the request
|
|
// is Android client with version less than 0.5.36
|
|
func shouldRejectRequest(c *gin.Context) (bool, error) {
|
|
userAgent := c.GetHeader("User-Agent")
|
|
clientVersion := c.GetHeader("X-Client-Version")
|
|
clientPkg := c.GetHeader("X-Client-Package")
|
|
|
|
if !strings.Contains(strings.ToLower(userAgent), "android") {
|
|
return false, nil
|
|
}
|
|
|
|
if clientPkg == "io.ente.photos.fdroid" {
|
|
return false, nil
|
|
}
|
|
|
|
versionSplit := strings.Split(clientVersion, ".")
|
|
|
|
if len(versionSplit) != 3 {
|
|
return false, nil
|
|
}
|
|
if versionSplit[0] != "0" {
|
|
return false, nil
|
|
}
|
|
minorVersion, err := strconv.Atoi(versionSplit[1])
|
|
if err != nil {
|
|
// avoid reject when parsing fails
|
|
return false, nil
|
|
}
|
|
patchVersion, err := strconv.Atoi(versionSplit[2])
|
|
if err != nil {
|
|
// avoid reject when parsing fails
|
|
return false, nil
|
|
}
|
|
shouldReject := minorVersion <= 5 && patchVersion <= 35
|
|
if shouldReject {
|
|
log.Warnf("request rejected from older client with version %s", clientVersion)
|
|
}
|
|
return shouldReject, nil
|
|
}
|
|
|
|
// GetDuplicates returns the list of files of the same size
|
|
func (h *FileHandler) GetDuplicates(c *gin.Context) {
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
dupes, err := h.Controller.GetDuplicates(userID)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"duplicates": dupes,
|
|
})
|
|
}
|
|
|
|
// GetLargeThumbnail returns the list of files whose thumbnail size is larger than threshold size
|
|
func (h *FileHandler) GetLargeThumbnailFiles(c *gin.Context) {
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
threshold, _ := strconv.ParseInt(c.Query("threshold"), 10, 64)
|
|
largeThumbnailFiles, err := h.Controller.GetLargeThumbnailFiles(userID, threshold)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"largeThumbnailFiles": largeThumbnailFiles,
|
|
})
|
|
}
|
|
|
|
// UpdateMagicMetadata updates magic metadata for a list of files.
|
|
func (h *FileHandler) UpdateMagicMetadata(c *gin.Context) {
|
|
var request ente.UpdateMultipleMagicMetadataRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
if len(request.MetadataList) > DefaultMaxBatchSize {
|
|
handler.Error(c, stacktrace.Propagate(ente.ErrBatchSizeTooLarge, ""))
|
|
return
|
|
}
|
|
err := h.Controller.UpdateMagicMetadata(c, request, false)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// UpdatePublicMagicMetadata updates public magic metadata for a list of files.
|
|
func (h *FileHandler) UpdatePublicMagicMetadata(c *gin.Context) {
|
|
var request ente.UpdateMultipleMagicMetadataRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
err := h.Controller.UpdateMagicMetadata(c, request, true)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// UpdateThumbnail updates thumbnail of a file
|
|
func (h *FileHandler) UpdateThumbnail(c *gin.Context) {
|
|
enteApp := auth.GetApp(c)
|
|
|
|
var request ente.UpdateThumbnailRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
err := h.Controller.UpdateThumbnail(c, request.FileID, request.Thumbnail, enteApp)
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
func (h *FileHandler) GetTotalFileCount(c *gin.Context) {
|
|
count, err := h.Controller.GetTotalFileCount()
|
|
if err != nil {
|
|
handler.Error(c, stacktrace.Propagate(err, ""))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"count": count,
|
|
})
|
|
}
|
|
|
|
func getUserAndFileIDs(c *gin.Context) (int64, int64) {
|
|
fileID, _ := strconv.ParseInt(c.Param("fileID"), 10, 64)
|
|
userID := auth.GetUserID(c.Request.Header)
|
|
return userID, fileID
|
|
}
|
|
|
|
// logBadRedirect will log the request id if we are redirecting to another url with the auth-token in header
|
|
func (h *FileHandler) logBadRedirect(c *gin.Context) {
|
|
if len(c.GetHeader("X-Auth-Token")) != 0 && os.Getenv("ENVIRONMENT") != "" {
|
|
log.WithField("req_id", requestid.Get(c)).Error("critical: sending token to another service")
|
|
}
|
|
}
|