From ad6e0ccb223e4d18e4af567edd17e2d9124d9de6 Mon Sep 17 00:00:00 2001 From: divyam234 <47589864+divyam234@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:30:32 +0530 Subject: [PATCH] feat: Add file and folder sharing --- api/router.go | 18 +- cmd/run.go | 1 + .../migrations/20240912164825_table.sql | 18 ++ .../migrations/20240915100057_modify.sql | 46 +++++ internal/utils/utils.go | 4 + pkg/controller/controller.go | 5 +- pkg/controller/file.go | 68 +++++++- pkg/controller/share.go | 66 +++++++ pkg/mapper/mapper.go | 1 - pkg/models/file.go | 1 - pkg/models/share.go | 15 ++ pkg/schemas/file.go | 24 ++- pkg/schemas/share.go | 13 ++ pkg/services/file.go | 120 ++++++++++--- pkg/services/share.go | 164 ++++++++++++++++++ 15 files changed, 530 insertions(+), 34 deletions(-) create mode 100644 internal/database/migrations/20240912164825_table.sql create mode 100644 internal/database/migrations/20240915100057_modify.sql create mode 100644 pkg/controller/share.go create mode 100644 pkg/models/share.go create mode 100644 pkg/schemas/share.go create mode 100644 pkg/services/share.go diff --git a/api/router.go b/api/router.go index f808964..af8f8b5 100644 --- a/api/router.go +++ b/api/router.go @@ -33,6 +33,10 @@ func InitRouter(r *gin.Engine, c *controller.Controller, cnf *config.Config, db files.HEAD(":fileID/download/:fileName", c.GetFileDownload) files.GET(":fileID/download/:fileName", c.GetFileDownload) files.PUT(":fileID/parts", authmiddleware, c.UpdateParts) + files.POST(":fileID/share", authmiddleware, c.CreateShare) + files.GET(":fileID/share", authmiddleware, c.GetShareByFileId) + files.PATCH(":fileID/share", authmiddleware, c.EditShare) + files.DELETE(":fileID/share", authmiddleware, c.DeleteShare) files.GET("/category/stats", authmiddleware, c.GetCategoryStats) files.POST("/move", authmiddleware, c.MoveFiles) files.POST("/directories", authmiddleware, c.MakeDirectory) @@ -44,9 +48,9 @@ func InitRouter(r *gin.Engine, c *controller.Controller, cnf *config.Config, db { uploads.Use(authmiddleware) uploads.GET("/stats", c.UploadStats) - uploads.GET(":id", c.GetUploadFileById) - uploads.POST(":id", c.UploadFile) - uploads.DELETE(":id", c.DeleteUploadFile) + uploads.GET("/:id", c.GetUploadFileById) + uploads.POST("/:id", c.UploadFile) + uploads.DELETE("/:id", c.DeleteUploadFile) } users := api.Group("/users") { @@ -60,6 +64,14 @@ func InitRouter(r *gin.Engine, c *controller.Controller, cnf *config.Config, db users.DELETE("/bots", c.RemoveBots) users.DELETE("/sessions/:id", c.RemoveSession) } + share := api.Group("/share") + { + share.GET("/:shareID", c.GetShareById) + share.GET("/:shareID/files", c.ListShareFiles) + share.GET("/:shareID/files/:fileID/stream/:fileName", c.StreamSharedFile) + share.GET("/:shareID/files/:fileID/download/:fileName", c.StreamSharedFile) + share.POST("/:shareID/unlock", c.ShareUnlock) + } } ui.AddRoutes(r) diff --git a/cmd/run.go b/cmd/run.go index 295cf74..0b45795 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -155,6 +155,7 @@ func runApplication(conf *config.Config) { services.NewFileService, services.NewUploadService, services.NewUserService, + services.NewShareService, controller.NewController, ), fx.Invoke( diff --git a/internal/database/migrations/20240912164825_table.sql b/internal/database/migrations/20240912164825_table.sql new file mode 100644 index 0000000..5909425 --- /dev/null +++ b/internal/database/migrations/20240912164825_table.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS teldrive.file_shares ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + file_id uuid NOT NULL, + password text NULL, + expires_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), + updated_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), + user_id bigint NOT NULL, + CONSTRAINT file_shares_pkey PRIMARY KEY (id), + CONSTRAINT fk_file FOREIGN KEY (file_id) REFERENCES teldrive.files (id) ON DELETE CASCADE +); + +CREATE INDEX idx_file_shares_file_id ON teldrive.file_shares USING btree (file_id); +ALTER TABLE teldrive.files DROP COLUMN IF EXISTS starred; + +-- +goose StatementEnd diff --git a/internal/database/migrations/20240915100057_modify.sql b/internal/database/migrations/20240915100057_modify.sql new file mode 100644 index 0000000..a9d19da --- /dev/null +++ b/internal/database/migrations/20240915100057_modify.sql @@ -0,0 +1,46 @@ +-- +goose Up +-- +goose StatementBegin +DROP INDEX IF EXISTS teldrive.idx_files_starred_updated_at; +CREATE OR REPLACE FUNCTION teldrive.create_directories(u_id bigint, long_path text) + RETURNS SETOF teldrive.files + LANGUAGE plpgsql +AS $function$ +DECLARE + path_parts TEXT[]; + current_directory_id UUID; + new_directory_id UUID; + directory_name TEXT; + path_so_far TEXT; +BEGIN + path_parts := string_to_array(regexp_replace(long_path, '^/+', ''), '/'); + + path_so_far := ''; + + SELECT id INTO current_directory_id + FROM teldrive.files + WHERE parent_id is NULL AND user_id = u_id AND type = 'folder'; + + FOR directory_name IN SELECT unnest(path_parts) LOOP + path_so_far := CONCAT(path_so_far, '/', directory_name); + + SELECT id INTO new_directory_id + FROM teldrive.files + WHERE parent_id = current_directory_id + AND "name" = directory_name + AND "user_id" = u_id; + + IF new_directory_id IS NULL THEN + INSERT INTO teldrive.files ("name", "type", mime_type, parent_id, "user_id") + VALUES (directory_name, 'folder', 'drive/folder', current_directory_id, u_id) + RETURNING id INTO new_directory_id; + END IF; + + current_directory_id := new_directory_id; + END LOOP; + + RETURN QUERY SELECT * FROM teldrive.files WHERE id = current_directory_id; +END; +$function$ +; +-- +goose StatementEnd + diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 77d8fcd..af7e6aa 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -64,6 +64,10 @@ func Int64Pointer(b int64) *int64 { return &b } +func StringPointer(b string) *string { + return &b +} + func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 093fdf3..f857bdb 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -9,16 +9,19 @@ type Controller struct { UserService *services.UserService UploadService *services.UploadService AuthService *services.AuthService + ShareService *services.ShareService } func NewController(fileService *services.FileService, userService *services.UserService, uploadService *services.UploadService, - authService *services.AuthService) *Controller { + authService *services.AuthService, + shareService *services.ShareService) *Controller { return &Controller{ FileService: fileService, UserService: userService, UploadService: uploadService, AuthService: authService, + ShareService: shareService, } } diff --git a/pkg/controller/file.go b/pkg/controller/file.go index 9ec8328..1351230 100644 --- a/pkg/controller/file.go +++ b/pkg/controller/file.go @@ -147,6 +147,70 @@ func (fc *Controller) DeleteFiles(c *gin.Context) { c.JSON(http.StatusOK, res) } +func (fc *Controller) CreateShare(c *gin.Context) { + + userId, _ := auth.GetUser(c) + + var payload schemas.FileShareIn + if err := c.ShouldBindJSON(&payload); err != nil { + httputil.NewError(c, http.StatusBadRequest, err) + return + } + + err := fc.FileService.CreateShare(c.Param("fileID"), userId, &payload) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.Status(http.StatusCreated) +} + +func (fc *Controller) EditShare(c *gin.Context) { + + userId, _ := auth.GetUser(c) + + var payload schemas.FileShareIn + if err := c.ShouldBindJSON(&payload); err != nil { + httputil.NewError(c, http.StatusBadRequest, err) + return + } + + err := fc.FileService.UpdateShare(c.Param("shareID"), userId, &payload) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.Status(http.StatusOK) +} + +func (fc *Controller) DeleteShare(c *gin.Context) { + + userId, _ := auth.GetUser(c) + + err := fc.FileService.DeleteShare(c.Param("fileID"), userId) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.Status(http.StatusNoContent) +} + +func (fc *Controller) GetShareByFileId(c *gin.Context) { + + userId, _ := auth.GetUser(c) + + res, err := fc.FileService.GetShareByFileId(c.Param("fileID"), userId) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.JSON(http.StatusOK, res) +} + func (fc *Controller) UpdateParts(c *gin.Context) { userId, _ := auth.GetUser(c) @@ -196,9 +260,9 @@ func (fc *Controller) GetCategoryStats(c *gin.Context) { } func (fc *Controller) GetFileStream(c *gin.Context) { - fc.FileService.GetFileStream(c, false) + fc.FileService.GetFileStream(c, false, nil) } func (fc *Controller) GetFileDownload(c *gin.Context) { - fc.FileService.GetFileStream(c, true) + fc.FileService.GetFileStream(c, true, nil) } diff --git a/pkg/controller/share.go b/pkg/controller/share.go new file mode 100644 index 0000000..865c8d1 --- /dev/null +++ b/pkg/controller/share.go @@ -0,0 +1,66 @@ +package controller + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/tgdrive/teldrive/pkg/httputil" + "github.com/tgdrive/teldrive/pkg/schemas" +) + +func (sc *Controller) GetShareById(c *gin.Context) { + + res, err := sc.ShareService.GetShareById(c.Param("shareID")) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.JSON(http.StatusOK, res) +} + +func (sc *Controller) ShareUnlock(c *gin.Context) { + var payload schemas.ShareAccess + if err := c.ShouldBindJSON(&payload); err != nil { + httputil.NewError(c, http.StatusBadRequest, err) + return + } + err := sc.ShareService.ShareUnlock(c.Param("shareID"), &payload) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.Status(http.StatusOK) +} + +func (sc *Controller) ListShareFiles(c *gin.Context) { + + query := schemas.ShareFileQuery{ + Limit: 500, + Page: 1, + Order: "asc", + Sort: "name", + } + + if err := c.ShouldBindQuery(&query); err != nil { + httputil.NewError(c, http.StatusBadRequest, err) + return + } + + res, err := sc.ShareService.ListShareFiles(c.Param("shareID"), &query, c.GetHeader("Authorization")) + if err != nil { + httputil.NewError(c, err.Code, err.Error) + return + } + + c.JSON(http.StatusOK, res) +} + +func (sc *Controller) StreamSharedFile(c *gin.Context) { + sc.ShareService.StreamSharedFile(c, false) +} + +func (sc *Controller) DownloadSharedFile(c *gin.Context) { + sc.ShareService.StreamSharedFile(c, true) +} diff --git a/pkg/mapper/mapper.go b/pkg/mapper/mapper.go index 6e34e05..366f8f2 100644 --- a/pkg/mapper/mapper.go +++ b/pkg/mapper/mapper.go @@ -18,7 +18,6 @@ func ToFileOut(file models.File) *schemas.FileOut { Category: file.Category, Encrypted: file.Encrypted, Size: size, - Starred: file.Starred, ParentID: file.ParentID.String, UpdatedAt: file.UpdatedAt, } diff --git a/pkg/models/file.go b/pkg/models/file.go index bb89933..871c1eb 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -14,7 +14,6 @@ type File struct { Type string `gorm:"type:text;not null"` MimeType string `gorm:"type:text;not null"` Size *int64 `gorm:"type:bigint"` - Starred bool `gorm:"default:false"` Category string `gorm:"type:text"` Encrypted bool `gorm:"default:false"` UserID int64 `gorm:"type:bigint;not null"` diff --git a/pkg/models/share.go b/pkg/models/share.go new file mode 100644 index 0000000..5d71ceb --- /dev/null +++ b/pkg/models/share.go @@ -0,0 +1,15 @@ +package models + +import ( + "time" +) + +type FileShare struct { + ID string `gorm:"type:uuid;default:uuid_generate_v4();primary_key"` + FileID string `gorm:"type:uuid;not null"` + Password *string `gorm:"type:text"` + ExpiresAt *time.Time `gorm:"type:timestamp"` + CreatedAt time.Time `gorm:"type:timestamp;not null;default:current_timestamp"` + UpdatedAt time.Time `gorm:"type:timestamp;not null;default:current_timestamp"` + UserID int64 `gorm:"type:bigint;not null"` +} diff --git a/pkg/schemas/file.go b/pkg/schemas/file.go index 5d3e1eb..11e51ee 100644 --- a/pkg/schemas/file.go +++ b/pkg/schemas/file.go @@ -16,7 +16,7 @@ type FileQuery struct { Path string `form:"path"` Op string `form:"op"` DeepSearch bool `form:"deepSearch"` - Starred *bool `form:"starred"` + Shared *bool `form:"shared"` ParentID string `form:"parentId"` Category string `form:"category"` UpdatedAt string `form:"updatedAt"` @@ -46,7 +46,6 @@ type FileOut struct { Category string `json:"category,omitempty"` Encrypted bool `json:"encrypted"` Size int64 `json:"size,omitempty"` - Starred bool `json:"starred"` ParentID string `json:"parentId,omitempty"` ParentPath string `json:"parentPath,omitempty"` UpdatedAt time.Time `json:"updatedAt,omitempty"` @@ -61,7 +60,6 @@ type FileOutFull struct { type FileUpdate struct { Name string `json:"name,omitempty"` - Starred *bool `json:"starred,omitempty"` UpdatedAt time.Time `json:"updatedAt,omitempty"` Parts []Part `json:"parts,omitempty"` Size *int64 `json:"size,omitempty"` @@ -112,3 +110,23 @@ type FileCategoryStats struct { TotalSize int `json:"totalSize"` Category string `json:"category"` } + +type FileShareIn struct { + Password string `json:"password,omitempty"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +type FileShareOut struct { + ID string `json:"id"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` + Protected bool `json:"protected"` + UserID int64 `json:"userId,omitempty"` +} + +type FileShare struct { + Password *string + ExpiresAt *time.Time + Type string + FileId string + UserId int64 +} diff --git a/pkg/schemas/share.go b/pkg/schemas/share.go new file mode 100644 index 0000000..b5e69f0 --- /dev/null +++ b/pkg/schemas/share.go @@ -0,0 +1,13 @@ +package schemas + +type ShareAccess struct { + Password string `json:"password" binding:"required"` +} + +type ShareFileQuery struct { + ParentID string `form:"parentId"` + Sort string `form:"sort"` + Order string `form:"order"` + Limit int `form:"limit"` + Page int `form:"page"` +} diff --git a/pkg/services/file.go b/pkg/services/file.go index b53bd86..aa5cc75 100644 --- a/pkg/services/file.go +++ b/pkg/services/file.go @@ -35,6 +35,7 @@ import ( "github.com/tgdrive/teldrive/pkg/schemas" "github.com/tgdrive/teldrive/pkg/types" "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" "gorm.io/datatypes" "gorm.io/gorm" @@ -141,7 +142,6 @@ func (fs *FileService) CreateFile(c *gin.Context, userId int64, fileIn *schemas. fileDB.MimeType = fileIn.MimeType fileDB.Category = string(category.GetCategory(fileIn.Name)) fileDB.Parts = datatypes.NewJSONSlice(fileIn.Parts) - fileDB.Starred = false fileDB.Size = &fileIn.Size } fileDB.Name = fileIn.Name @@ -174,10 +174,6 @@ func (fs *FileService) UpdateFile(id string, userId int64, update *schemas.FileU Size: update.Size, } - if update.Starred != nil { - updateDb.Starred = *update.Starred - } - if len(update.Parts) > 0 { updateDb.Parts = datatypes.NewJSONSlice(update.Parts) } @@ -286,8 +282,9 @@ func (fs *FileService) ListFiles(userId int64, fquery *schemas.FileQuery) (*sche if fquery.Type != "" { query.Where("type = ?", fquery.Type) } - if fquery.Starred != nil { - query.Where("starred = ?", *fquery.Starred) + + if fquery.Shared != nil && *fquery.Shared { + query.Where("id in (SELECT file_id FROM teldrive.file_shares where user_id = ?)", userId) } } @@ -411,6 +408,78 @@ func (fs *FileService) DeleteFiles(userId int64, payload *schemas.DeleteOperatio return &schemas.Message{Message: "files deleted"}, nil } +func (fs *FileService) CreateShare(fileId string, userId int64, payload *schemas.FileShareIn) *types.AppError { + + var fileShare models.FileShare + + if payload.Password != "" { + bytes, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.MinCost) + if err != nil { + return &types.AppError{Error: err} + } + fileShare.Password = utils.StringPointer(string(bytes)) + } + + fileShare.FileID = fileId + fileShare.ExpiresAt = payload.ExpiresAt + fileShare.UserID = userId + + if err := fs.db.Create(&fileShare).Error; err != nil { + return &types.AppError{Error: err} + } + + return nil +} + +func (fs *FileService) UpdateShare(fileId string, userId int64, payload *schemas.FileShareIn) *types.AppError { + + var fileShareUpdate models.FileShare + + if payload.Password != "" { + bytes, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.MinCost) + if err != nil { + return &types.AppError{Error: err} + } + fileShareUpdate.Password = utils.StringPointer(string(bytes)) + } + + fileShareUpdate.ExpiresAt = payload.ExpiresAt + + if err := fs.db.Model(&models.FileShare{}).Where("file_id = ?", fileId).Where("user_id = ?", userId). + Updates(fileShareUpdate).Error; err != nil { + return &types.AppError{Error: err} + } + + return nil +} + +func (fs *FileService) GetShareByFileId(fileId string, userId int64) (*schemas.FileShareOut, *types.AppError) { + + var result []models.FileShare + + if err := fs.db.Model(&models.FileShare{}).Where("file_id = ?", fileId).Where("user_id = ?", userId). + Find(&result).Error; err != nil { + return nil, &types.AppError{Error: err} + } + + if len(result) == 0 { + return nil, nil + } + + res := &schemas.FileShareOut{ID: result[0].ID, ExpiresAt: result[0].ExpiresAt, Protected: result[0].Password != nil} + + return res, nil +} + +func (fs *FileService) DeleteShare(fileId string, userId int64) *types.AppError { + + if err := fs.db.Where("file_id = ?", fileId).Where("user_id = ?", userId).Delete(&models.FileShare{}).Error; err != nil { + return &types.AppError{Error: err} + } + + return nil +} + func (fs *FileService) UpdateParts(c *gin.Context, id string, userId int64, payload *schemas.PartUpdate) (*schemas.Message, *types.AppError) { var file models.File @@ -591,7 +660,6 @@ func (fs *FileService) CopyFile(c *gin.Context) (*schemas.FileOut, *types.AppErr dbFile.MimeType = file.MimeType dbFile.Parts = datatypes.NewJSONSlice(newIds) dbFile.UserID = userId - dbFile.Starred = false dbFile.Status = "active" dbFile.ParentID = sql.NullString{ String: dest.Id, @@ -608,7 +676,7 @@ func (fs *FileService) CopyFile(c *gin.Context) (*schemas.FileOut, *types.AppErr return mapper.ToFileOut(dbFile), nil } -func (fs *FileService) GetFileStream(c *gin.Context, download bool) { +func (fs *FileService) GetFileStream(c *gin.Context, download bool, sharedFile *schemas.FileShareOut) { w := c.Writer @@ -616,8 +684,6 @@ func (fs *FileService) GetFileStream(c *gin.Context, download bool) { fileID := c.Param("fileID") - authHash := c.Query("hash") - var ( session *models.Session err error @@ -625,20 +691,28 @@ func (fs *FileService) GetFileStream(c *gin.Context, download bool) { user *types.JWTClaims ) - if authHash == "" { - user, err = auth.VerifyUser(c, fs.db, fs.cache, fs.cnf.JWT.Secret) - if err != nil { - http.Error(w, "missing session or authash", http.StatusUnauthorized) - return + if sharedFile == nil { + authHash := c.Query("hash") + + if authHash == "" { + user, err = auth.VerifyUser(c, fs.db, fs.cache, fs.cnf.JWT.Secret) + if err != nil { + http.Error(w, "missing session or authash", http.StatusUnauthorized) + return + } + userId, _ := strconv.ParseInt(user.Subject, 10, 64) + session = &models.Session{UserId: userId, Session: user.TgSession} + } else { + session, err = auth.GetSessionByHash(fs.db, fs.cache, authHash) + if err != nil { + http.Error(w, "invalid hash", http.StatusBadRequest) + return + } } - userId, _ := strconv.ParseInt(user.Subject, 10, 64) - session = &models.Session{UserId: userId, Session: user.TgSession} + } else { - session, err = auth.GetSessionByHash(fs.db, fs.cache, authHash) - if err != nil { - http.Error(w, "invalid hash", http.StatusBadRequest) - return - } + + session = &models.Session{UserId: sharedFile.UserID} } file := &schemas.FileOutFull{} diff --git a/pkg/services/share.go b/pkg/services/share.go new file mode 100644 index 0000000..c980b22 --- /dev/null +++ b/pkg/services/share.go @@ -0,0 +1,164 @@ +package services + +import ( + "encoding/base64" + "errors" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/tgdrive/teldrive/internal/cache" + "github.com/tgdrive/teldrive/internal/database" + "github.com/tgdrive/teldrive/pkg/mapper" + "github.com/tgdrive/teldrive/pkg/models" + "github.com/tgdrive/teldrive/pkg/schemas" + "github.com/tgdrive/teldrive/pkg/types" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type ShareService struct { + db *gorm.DB + fs *FileService + cache cache.Cacher +} + +var ( + ErrShareNotFound = errors.New("share not found") + ErrInvalidPassword = errors.New("invalid password") + ErrShareExpired = errors.New("share expired") +) + +func NewShareService(db *gorm.DB, fs *FileService, cache cache.Cacher) *ShareService { + return &ShareService{db: db, fs: fs, cache: cache} +} + +func (ss *ShareService) GetShareById(shareId string) (*schemas.FileShareOut, *types.AppError) { + + var result []models.FileShare + + if err := ss.db.Model(&models.FileShare{}).Where("id = ?", shareId).Find(&result).Error; err != nil { + return nil, &types.AppError{Error: err} + } + + if len(result) == 0 { + return nil, &types.AppError{Error: ErrShareNotFound, Code: http.StatusNotFound} + } + + if result[0].ExpiresAt != nil && result[0].ExpiresAt.Before(time.Now().UTC()) { + return nil, &types.AppError{Error: ErrShareExpired, Code: http.StatusNotFound} + } + + res := &schemas.FileShareOut{ + ExpiresAt: result[0].ExpiresAt, + Protected: result[0].Password != nil, + UserID: result[0].UserID, + } + + return res, nil +} + +func (ss *ShareService) ShareUnlock(shareId string, payload *schemas.ShareAccess) *types.AppError { + + var result []models.FileShare + + if err := ss.db.Model(&models.FileShare{}).Where("id = ?", shareId).Find(&result).Error; err != nil { + return &types.AppError{Error: err} + } + + if len(result) == 0 { + return &types.AppError{Error: ErrShareNotFound, Code: http.StatusNotFound} + } + + if err := bcrypt.CompareHashAndPassword([]byte(*result[0].Password), []byte(payload.Password)); err != nil { + return &types.AppError{Error: ErrInvalidPassword, Code: http.StatusUnauthorized} + } + return nil +} + +func (ss *ShareService) ListShareFiles(shareId string, query *schemas.ShareFileQuery, auth string) (*schemas.FileResponse, *types.AppError) { + + var ( + userId int64 + fileType string + fileId string + ) + + var result []schemas.FileShare + + key := "shares:" + shareId + + if err := ss.cache.Get(key, &result); err != nil { + if err := ss.db.Model(&models.FileShare{}).Where("file_shares.id = ?", shareId). + Select("file_shares.*", "f.type"). + Joins("left join teldrive.files as f on f.id = file_shares.file_id"). + Scan(&result).Error; err != nil { + return nil, &types.AppError{Error: err} + } + + if len(result) == 0 { + return nil, &types.AppError{Error: ErrShareNotFound, Code: http.StatusNotFound} + } + ss.cache.Set(key, result, 0) + } + + if result[0].Password != nil { + if auth == "" { + return nil, &types.AppError{Error: ErrInvalidPassword, Code: http.StatusUnauthorized} + } + bytes, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")) + password := strings.Split(string(bytes), ":")[1] + if err != nil { + return nil, &types.AppError{Error: err} + } + if err := bcrypt.CompareHashAndPassword([]byte(*result[0].Password), []byte(password)); err != nil { + return nil, &types.AppError{Error: ErrInvalidPassword, Code: http.StatusUnauthorized} + } + + } + + userId = result[0].UserId + + fileType = "folder" + + fileId = query.ParentID + + if query.ParentID == "" { + fileType = result[0].Type + fileId = result[0].FileId + } + + if fileType == "folder" { + return ss.fs.ListFiles(userId, &schemas.FileQuery{ + ParentID: fileId, + Limit: query.Limit, + Page: query.Page, + Order: query.Order, + Sort: query.Sort, + Op: "list"}) + } else { + var file models.File + if err := ss.db.Where("id = ?", fileId).First(&file).Error; err != nil { + if database.IsRecordNotFoundErr(err) { + return nil, &types.AppError{Error: database.ErrNotFound, Code: http.StatusNotFound} + } + return nil, &types.AppError{Error: err} + } + return &schemas.FileResponse{Files: []schemas.FileOut{*mapper.ToFileOut(file)}}, nil + } + +} + +func (ss *ShareService) StreamSharedFile(c *gin.Context, download bool) { + + shareID := c.Param("shareID") + + res, err := ss.GetShareById(shareID) + + if err != nil { + http.Error(c.Writer, err.Error.Error(), err.Code) + return + } + ss.fs.GetFileStream(c, download, res) +}