mirror of
https://github.com/tgdrive/teldrive.git
synced 2024-09-20 08:15:55 +08:00
feat: Add file and folder sharing
This commit is contained in:
parent
a37ac524da
commit
ad6e0ccb22
|
@ -33,6 +33,10 @@ func InitRouter(r *gin.Engine, c *controller.Controller, cnf *config.Config, db
|
||||||
files.HEAD(":fileID/download/:fileName", c.GetFileDownload)
|
files.HEAD(":fileID/download/:fileName", c.GetFileDownload)
|
||||||
files.GET(":fileID/download/:fileName", c.GetFileDownload)
|
files.GET(":fileID/download/:fileName", c.GetFileDownload)
|
||||||
files.PUT(":fileID/parts", authmiddleware, c.UpdateParts)
|
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.GET("/category/stats", authmiddleware, c.GetCategoryStats)
|
||||||
files.POST("/move", authmiddleware, c.MoveFiles)
|
files.POST("/move", authmiddleware, c.MoveFiles)
|
||||||
files.POST("/directories", authmiddleware, c.MakeDirectory)
|
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.Use(authmiddleware)
|
||||||
uploads.GET("/stats", c.UploadStats)
|
uploads.GET("/stats", c.UploadStats)
|
||||||
uploads.GET(":id", c.GetUploadFileById)
|
uploads.GET("/:id", c.GetUploadFileById)
|
||||||
uploads.POST(":id", c.UploadFile)
|
uploads.POST("/:id", c.UploadFile)
|
||||||
uploads.DELETE(":id", c.DeleteUploadFile)
|
uploads.DELETE("/:id", c.DeleteUploadFile)
|
||||||
}
|
}
|
||||||
users := api.Group("/users")
|
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("/bots", c.RemoveBots)
|
||||||
users.DELETE("/sessions/:id", c.RemoveSession)
|
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)
|
ui.AddRoutes(r)
|
||||||
|
|
|
@ -155,6 +155,7 @@ func runApplication(conf *config.Config) {
|
||||||
services.NewFileService,
|
services.NewFileService,
|
||||||
services.NewUploadService,
|
services.NewUploadService,
|
||||||
services.NewUserService,
|
services.NewUserService,
|
||||||
|
services.NewShareService,
|
||||||
controller.NewController,
|
controller.NewController,
|
||||||
),
|
),
|
||||||
fx.Invoke(
|
fx.Invoke(
|
||||||
|
|
18
internal/database/migrations/20240912164825_table.sql
Normal file
18
internal/database/migrations/20240912164825_table.sql
Normal file
|
@ -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
|
46
internal/database/migrations/20240915100057_modify.sql
Normal file
46
internal/database/migrations/20240915100057_modify.sql
Normal file
|
@ -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
|
||||||
|
|
|
@ -64,6 +64,10 @@ func Int64Pointer(b int64) *int64 {
|
||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringPointer(b string) *string {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
func PathExists(path string) (bool, error) {
|
func PathExists(path string) (bool, error) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -9,16 +9,19 @@ type Controller struct {
|
||||||
UserService *services.UserService
|
UserService *services.UserService
|
||||||
UploadService *services.UploadService
|
UploadService *services.UploadService
|
||||||
AuthService *services.AuthService
|
AuthService *services.AuthService
|
||||||
|
ShareService *services.ShareService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(fileService *services.FileService,
|
func NewController(fileService *services.FileService,
|
||||||
userService *services.UserService,
|
userService *services.UserService,
|
||||||
uploadService *services.UploadService,
|
uploadService *services.UploadService,
|
||||||
authService *services.AuthService) *Controller {
|
authService *services.AuthService,
|
||||||
|
shareService *services.ShareService) *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
UploadService: uploadService,
|
UploadService: uploadService,
|
||||||
AuthService: authService,
|
AuthService: authService,
|
||||||
|
ShareService: shareService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,70 @@ func (fc *Controller) DeleteFiles(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, res)
|
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) {
|
func (fc *Controller) UpdateParts(c *gin.Context) {
|
||||||
|
|
||||||
userId, _ := auth.GetUser(c)
|
userId, _ := auth.GetUser(c)
|
||||||
|
@ -196,9 +260,9 @@ func (fc *Controller) GetCategoryStats(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fc *Controller) GetFileStream(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) {
|
func (fc *Controller) GetFileDownload(c *gin.Context) {
|
||||||
fc.FileService.GetFileStream(c, true)
|
fc.FileService.GetFileStream(c, true, nil)
|
||||||
}
|
}
|
||||||
|
|
66
pkg/controller/share.go
Normal file
66
pkg/controller/share.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ func ToFileOut(file models.File) *schemas.FileOut {
|
||||||
Category: file.Category,
|
Category: file.Category,
|
||||||
Encrypted: file.Encrypted,
|
Encrypted: file.Encrypted,
|
||||||
Size: size,
|
Size: size,
|
||||||
Starred: file.Starred,
|
|
||||||
ParentID: file.ParentID.String,
|
ParentID: file.ParentID.String,
|
||||||
UpdatedAt: file.UpdatedAt,
|
UpdatedAt: file.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ type File struct {
|
||||||
Type string `gorm:"type:text;not null"`
|
Type string `gorm:"type:text;not null"`
|
||||||
MimeType string `gorm:"type:text;not null"`
|
MimeType string `gorm:"type:text;not null"`
|
||||||
Size *int64 `gorm:"type:bigint"`
|
Size *int64 `gorm:"type:bigint"`
|
||||||
Starred bool `gorm:"default:false"`
|
|
||||||
Category string `gorm:"type:text"`
|
Category string `gorm:"type:text"`
|
||||||
Encrypted bool `gorm:"default:false"`
|
Encrypted bool `gorm:"default:false"`
|
||||||
UserID int64 `gorm:"type:bigint;not null"`
|
UserID int64 `gorm:"type:bigint;not null"`
|
||||||
|
|
15
pkg/models/share.go
Normal file
15
pkg/models/share.go
Normal file
|
@ -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"`
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type FileQuery struct {
|
||||||
Path string `form:"path"`
|
Path string `form:"path"`
|
||||||
Op string `form:"op"`
|
Op string `form:"op"`
|
||||||
DeepSearch bool `form:"deepSearch"`
|
DeepSearch bool `form:"deepSearch"`
|
||||||
Starred *bool `form:"starred"`
|
Shared *bool `form:"shared"`
|
||||||
ParentID string `form:"parentId"`
|
ParentID string `form:"parentId"`
|
||||||
Category string `form:"category"`
|
Category string `form:"category"`
|
||||||
UpdatedAt string `form:"updatedAt"`
|
UpdatedAt string `form:"updatedAt"`
|
||||||
|
@ -46,7 +46,6 @@ type FileOut struct {
|
||||||
Category string `json:"category,omitempty"`
|
Category string `json:"category,omitempty"`
|
||||||
Encrypted bool `json:"encrypted"`
|
Encrypted bool `json:"encrypted"`
|
||||||
Size int64 `json:"size,omitempty"`
|
Size int64 `json:"size,omitempty"`
|
||||||
Starred bool `json:"starred"`
|
|
||||||
ParentID string `json:"parentId,omitempty"`
|
ParentID string `json:"parentId,omitempty"`
|
||||||
ParentPath string `json:"parentPath,omitempty"`
|
ParentPath string `json:"parentPath,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
|
@ -61,7 +60,6 @@ type FileOutFull struct {
|
||||||
|
|
||||||
type FileUpdate struct {
|
type FileUpdate struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Starred *bool `json:"starred,omitempty"`
|
|
||||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
Parts []Part `json:"parts,omitempty"`
|
Parts []Part `json:"parts,omitempty"`
|
||||||
Size *int64 `json:"size,omitempty"`
|
Size *int64 `json:"size,omitempty"`
|
||||||
|
@ -112,3 +110,23 @@ type FileCategoryStats struct {
|
||||||
TotalSize int `json:"totalSize"`
|
TotalSize int `json:"totalSize"`
|
||||||
Category string `json:"category"`
|
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
|
||||||
|
}
|
||||||
|
|
13
pkg/schemas/share.go
Normal file
13
pkg/schemas/share.go
Normal file
|
@ -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"`
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"github.com/tgdrive/teldrive/pkg/schemas"
|
"github.com/tgdrive/teldrive/pkg/schemas"
|
||||||
"github.com/tgdrive/teldrive/pkg/types"
|
"github.com/tgdrive/teldrive/pkg/types"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -141,7 +142,6 @@ func (fs *FileService) CreateFile(c *gin.Context, userId int64, fileIn *schemas.
|
||||||
fileDB.MimeType = fileIn.MimeType
|
fileDB.MimeType = fileIn.MimeType
|
||||||
fileDB.Category = string(category.GetCategory(fileIn.Name))
|
fileDB.Category = string(category.GetCategory(fileIn.Name))
|
||||||
fileDB.Parts = datatypes.NewJSONSlice(fileIn.Parts)
|
fileDB.Parts = datatypes.NewJSONSlice(fileIn.Parts)
|
||||||
fileDB.Starred = false
|
|
||||||
fileDB.Size = &fileIn.Size
|
fileDB.Size = &fileIn.Size
|
||||||
}
|
}
|
||||||
fileDB.Name = fileIn.Name
|
fileDB.Name = fileIn.Name
|
||||||
|
@ -174,10 +174,6 @@ func (fs *FileService) UpdateFile(id string, userId int64, update *schemas.FileU
|
||||||
Size: update.Size,
|
Size: update.Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.Starred != nil {
|
|
||||||
updateDb.Starred = *update.Starred
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(update.Parts) > 0 {
|
if len(update.Parts) > 0 {
|
||||||
updateDb.Parts = datatypes.NewJSONSlice(update.Parts)
|
updateDb.Parts = datatypes.NewJSONSlice(update.Parts)
|
||||||
}
|
}
|
||||||
|
@ -286,8 +282,9 @@ func (fs *FileService) ListFiles(userId int64, fquery *schemas.FileQuery) (*sche
|
||||||
if fquery.Type != "" {
|
if fquery.Type != "" {
|
||||||
query.Where("type = ?", 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
|
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) {
|
func (fs *FileService) UpdateParts(c *gin.Context, id string, userId int64, payload *schemas.PartUpdate) (*schemas.Message, *types.AppError) {
|
||||||
|
|
||||||
var file models.File
|
var file models.File
|
||||||
|
@ -591,7 +660,6 @@ func (fs *FileService) CopyFile(c *gin.Context) (*schemas.FileOut, *types.AppErr
|
||||||
dbFile.MimeType = file.MimeType
|
dbFile.MimeType = file.MimeType
|
||||||
dbFile.Parts = datatypes.NewJSONSlice(newIds)
|
dbFile.Parts = datatypes.NewJSONSlice(newIds)
|
||||||
dbFile.UserID = userId
|
dbFile.UserID = userId
|
||||||
dbFile.Starred = false
|
|
||||||
dbFile.Status = "active"
|
dbFile.Status = "active"
|
||||||
dbFile.ParentID = sql.NullString{
|
dbFile.ParentID = sql.NullString{
|
||||||
String: dest.Id,
|
String: dest.Id,
|
||||||
|
@ -608,7 +676,7 @@ func (fs *FileService) CopyFile(c *gin.Context) (*schemas.FileOut, *types.AppErr
|
||||||
return mapper.ToFileOut(dbFile), nil
|
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
|
w := c.Writer
|
||||||
|
|
||||||
|
@ -616,8 +684,6 @@ func (fs *FileService) GetFileStream(c *gin.Context, download bool) {
|
||||||
|
|
||||||
fileID := c.Param("fileID")
|
fileID := c.Param("fileID")
|
||||||
|
|
||||||
authHash := c.Query("hash")
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
session *models.Session
|
session *models.Session
|
||||||
err error
|
err error
|
||||||
|
@ -625,20 +691,28 @@ func (fs *FileService) GetFileStream(c *gin.Context, download bool) {
|
||||||
user *types.JWTClaims
|
user *types.JWTClaims
|
||||||
)
|
)
|
||||||
|
|
||||||
if authHash == "" {
|
if sharedFile == nil {
|
||||||
user, err = auth.VerifyUser(c, fs.db, fs.cache, fs.cnf.JWT.Secret)
|
authHash := c.Query("hash")
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "missing session or authash", http.StatusUnauthorized)
|
if authHash == "" {
|
||||||
return
|
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 {
|
} else {
|
||||||
session, err = auth.GetSessionByHash(fs.db, fs.cache, authHash)
|
|
||||||
if err != nil {
|
session = &models.Session{UserId: sharedFile.UserID}
|
||||||
http.Error(w, "invalid hash", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &schemas.FileOutFull{}
|
file := &schemas.FileOutFull{}
|
||||||
|
|
164
pkg/services/share.go
Normal file
164
pkg/services/share.go
Normal file
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in a new issue