From e4c902dd9396b861d68d0ded5366bb0a0af45fbe Mon Sep 17 00:00:00 2001 From: KirCute <951206789@qq.com> Date: Tue, 19 Aug 2025 15:10:02 +0800 Subject: [PATCH] feat(share): support more secure file sharing (#991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供一种类似大多数网盘的文件分享操作,这种分享方式可以通过强制 Web 代理隐藏文件源路径,可以设置分享码、最大访问数和过期时间,并且不需要启用 guest 用户。 在全局设置中可以调整: - 是否强制 Web 代理 - 是否允许预览 - 是否允许预览压缩文件 - 分享文件后,点击“复制链接”按钮复制的内容 前端部分:OpenListTeam/OpenList-Frontend#156 文档部分:OpenListTeam/OpenList-Docs#130 Close #183 Close #526 Close #860 Close #892 Close #1079 * feat(share): support more secure file sharing * feat(share): add archive preview * fix(share): fix some bugs * feat(openlist_share): add openlist share driver * fix(share): lack unwrap when get virtual path * fix: use unwrapPath instead of path for virtual file name comparison * fix(share): change request method of /api/share/list from GET to Any * fix(share): path traversal vulnerability in sharing path check * 修复分享alias驱动的文件 没开代理时无法获取URL * fix(sharing): update error message for sharing root link extraction --------- Co-authored-by: Suyunmeng <69945917+Suyunmeng@users.noreply.github.com> Co-authored-by: j2rong4cn --- drivers/all.go | 1 + drivers/openlist_share/driver.go | 181 ++++++++++ drivers/openlist_share/meta.go | 27 ++ drivers/openlist_share/types.go | 111 ++++++ drivers/openlist_share/util.go | 32 ++ internal/bootstrap/data/setting.go | 5 + internal/bootstrap/data/user.go | 4 +- internal/conf/const.go | 6 + internal/db/db.go | 2 +- internal/db/sharing.go | 62 ++++ internal/errs/errors.go | 4 + internal/fs/fs.go | 2 +- internal/model/args.go | 20 ++ internal/model/sharing.go | 47 +++ internal/model/user.go | 5 + internal/op/sharing.go | 139 ++++++++ internal/sharing/archive.go | 65 ++++ internal/sharing/get.go | 60 ++++ internal/sharing/link.go | 46 +++ internal/sharing/list.go | 83 +++++ internal/sharing/sharing.go | 58 +++ server/handles/archive.go | 76 ++-- server/handles/fsread.go | 30 +- server/handles/sharing.go | 551 +++++++++++++++++++++++++++++ server/middlewares/auth.go | 90 ++--- server/middlewares/down.go | 9 +- server/middlewares/sharing.go | 18 + server/router.go | 58 ++- 28 files changed, 1698 insertions(+), 94 deletions(-) create mode 100644 drivers/openlist_share/driver.go create mode 100644 drivers/openlist_share/meta.go create mode 100644 drivers/openlist_share/types.go create mode 100644 drivers/openlist_share/util.go create mode 100644 internal/db/sharing.go create mode 100644 internal/model/sharing.go create mode 100644 internal/op/sharing.go create mode 100644 internal/sharing/archive.go create mode 100644 internal/sharing/get.go create mode 100644 internal/sharing/link.go create mode 100644 internal/sharing/list.go create mode 100644 internal/sharing/sharing.go create mode 100644 server/handles/sharing.go create mode 100644 server/middlewares/sharing.go diff --git a/drivers/all.go b/drivers/all.go index 2eb17b9e..5b274eab 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -48,6 +48,7 @@ import ( _ "github.com/OpenListTeam/OpenList/v4/drivers/onedrive_app" _ "github.com/OpenListTeam/OpenList/v4/drivers/onedrive_sharelink" _ "github.com/OpenListTeam/OpenList/v4/drivers/openlist" + _ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share" _ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak" _ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share" _ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open" diff --git a/drivers/openlist_share/driver.go b/drivers/openlist_share/driver.go new file mode 100644 index 00000000..41698777 --- /dev/null +++ b/drivers/openlist_share/driver.go @@ -0,0 +1,181 @@ +package openlist_share + +import ( + "context" + "fmt" + "net/http" + "net/url" + stdpath "path" + "strings" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/go-resty/resty/v2" +) + +type OpenListShare struct { + model.Storage + Addition + serverArchivePreview bool +} + +func (d *OpenListShare) Config() driver.Config { + return config +} + +func (d *OpenListShare) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *OpenListShare) Init(ctx context.Context) error { + d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/") + var settings common.Resp[map[string]string] + _, _, err := d.request("/public/settings", http.MethodGet, func(req *resty.Request) { + req.SetResult(&settings) + }) + if err != nil { + return err + } + d.serverArchivePreview = settings.Data["share_archive_preview"] == "true" + return nil +} + +func (d *OpenListShare) Drop(ctx context.Context) error { + return nil +} + +func (d *OpenListShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var resp common.Resp[FsListResp] + _, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ListReq{ + PageReq: model.PageReq{ + Page: 1, + PerPage: 0, + }, + Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), dir.GetPath()), + Password: d.Pwd, + Refresh: false, + }) + }) + if err != nil { + return nil, err + } + var files []model.Obj + for _, f := range resp.Data.Content { + file := model.ObjThumb{ + Object: model.Object{ + Name: f.Name, + Modified: f.Modified, + Ctime: f.Created, + Size: f.Size, + IsFolder: f.IsDir, + HashInfo: utils.FromString(f.HashInfo), + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, + } + files = append(files, &file) + } + return files, nil +} + +func (d *OpenListShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + path := utils.FixAndCleanPath(stdpath.Join(d.ShareId, file.GetPath())) + u := fmt.Sprintf("%s/sd%s?pwd=%s", d.Address, path, d.Pwd) + return &model.Link{URL: u}, nil +} + +func (d *OpenListShare) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { + if !d.serverArchivePreview || !d.ForwardArchiveReq { + return nil, errs.NotImplement + } + var resp common.Resp[ArchiveMetaResp] + _, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ArchiveMetaReq{ + ArchivePass: args.Password, + Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), obj.GetPath()), + Password: d.Pwd, + Refresh: false, + }) + }) + if code == 202 { + return nil, errs.WrongArchivePassword + } + if err != nil { + return nil, err + } + var tree []model.ObjTree + if resp.Data.Content != nil { + tree = make([]model.ObjTree, 0, len(resp.Data.Content)) + for _, content := range resp.Data.Content { + tree = append(tree, &content) + } + } + return &model.ArchiveMetaInfo{ + Comment: resp.Data.Comment, + Encrypted: resp.Data.Encrypted, + Tree: tree, + }, nil +} + +func (d *OpenListShare) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { + if !d.serverArchivePreview || !d.ForwardArchiveReq { + return nil, errs.NotImplement + } + var resp common.Resp[ArchiveListResp] + _, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) { + req.SetResult(&resp).SetBody(ArchiveListReq{ + ArchiveMetaReq: ArchiveMetaReq{ + ArchivePass: args.Password, + Path: stdpath.Join(fmt.Sprintf("/@s/%s", d.ShareId), obj.GetPath()), + Password: d.Pwd, + Refresh: false, + }, + PageReq: model.PageReq{ + Page: 1, + PerPage: 0, + }, + InnerPath: args.InnerPath, + }) + }) + if code == 202 { + return nil, errs.WrongArchivePassword + } + if err != nil { + return nil, err + } + var files []model.Obj + for _, f := range resp.Data.Content { + file := model.ObjThumb{ + Object: model.Object{ + Name: f.Name, + Modified: f.Modified, + Ctime: f.Created, + Size: f.Size, + IsFolder: f.IsDir, + HashInfo: utils.FromString(f.HashInfo), + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, + } + files = append(files, &file) + } + return files, nil +} + +func (d *OpenListShare) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { + if !d.serverArchivePreview || !d.ForwardArchiveReq { + return nil, errs.NotSupport + } + path := utils.FixAndCleanPath(stdpath.Join(d.ShareId, obj.GetPath())) + u := fmt.Sprintf("%s/sad%s?pwd=%s&inner=%s&pass=%s", + d.Address, + path, + d.Pwd, + utils.EncodePath(args.InnerPath, true), + url.QueryEscape(args.Password)) + return &model.Link{URL: u}, nil +} + +var _ driver.Driver = (*OpenListShare)(nil) diff --git a/drivers/openlist_share/meta.go b/drivers/openlist_share/meta.go new file mode 100644 index 00000000..cad4903a --- /dev/null +++ b/drivers/openlist_share/meta.go @@ -0,0 +1,27 @@ +package openlist_share + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/op" +) + +type Addition struct { + driver.RootPath + Address string `json:"url" required:"true"` + ShareId string `json:"sid" required:"true"` + Pwd string `json:"pwd"` + ForwardArchiveReq bool `json:"forward_archive_requests" default:"true"` +} + +var config = driver.Config{ + Name: "OpenListShare", + LocalSort: true, + NoUpload: true, + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &OpenListShare{} + }) +} diff --git a/drivers/openlist_share/types.go b/drivers/openlist_share/types.go new file mode 100644 index 00000000..a31c4632 --- /dev/null +++ b/drivers/openlist_share/types.go @@ -0,0 +1,111 @@ +package openlist_share + +import ( + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" +) + +type ListReq struct { + model.PageReq + Path string `json:"path" form:"path"` + Password string `json:"password" form:"password"` + Refresh bool `json:"refresh"` +} + +type ObjResp struct { + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"is_dir"` + Modified time.Time `json:"modified"` + Created time.Time `json:"created"` + Sign string `json:"sign"` + Thumb string `json:"thumb"` + Type int `json:"type"` + HashInfo string `json:"hashinfo"` +} + +type FsListResp struct { + Content []ObjResp `json:"content"` + Total int64 `json:"total"` + Readme string `json:"readme"` + Write bool `json:"write"` + Provider string `json:"provider"` +} + +type ArchiveMetaReq struct { + ArchivePass string `json:"archive_pass"` + Password string `json:"password"` + Path string `json:"path"` + Refresh bool `json:"refresh"` +} + +type TreeResp struct { + ObjResp + Children []TreeResp `json:"children"` + hashCache *utils.HashInfo +} + +func (t *TreeResp) GetSize() int64 { + return t.Size +} + +func (t *TreeResp) GetName() string { + return t.Name +} + +func (t *TreeResp) ModTime() time.Time { + return t.Modified +} + +func (t *TreeResp) CreateTime() time.Time { + return t.Created +} + +func (t *TreeResp) IsDir() bool { + return t.ObjResp.IsDir +} + +func (t *TreeResp) GetHash() utils.HashInfo { + return utils.FromString(t.HashInfo) +} + +func (t *TreeResp) GetID() string { + return "" +} + +func (t *TreeResp) GetPath() string { + return "" +} + +func (t *TreeResp) GetChildren() []model.ObjTree { + ret := make([]model.ObjTree, 0, len(t.Children)) + for _, child := range t.Children { + ret = append(ret, &child) + } + return ret +} + +func (t *TreeResp) Thumb() string { + return t.ObjResp.Thumb +} + +type ArchiveMetaResp struct { + Comment string `json:"comment"` + Encrypted bool `json:"encrypted"` + Content []TreeResp `json:"content"` + RawURL string `json:"raw_url"` + Sign string `json:"sign"` +} + +type ArchiveListReq struct { + model.PageReq + ArchiveMetaReq + InnerPath string `json:"inner_path"` +} + +type ArchiveListResp struct { + Content []ObjResp `json:"content"` + Total int64 `json:"total"` +} diff --git a/drivers/openlist_share/util.go b/drivers/openlist_share/util.go new file mode 100644 index 00000000..84864059 --- /dev/null +++ b/drivers/openlist_share/util.go @@ -0,0 +1,32 @@ +package openlist_share + +import ( + "fmt" + + "github.com/OpenListTeam/OpenList/v4/drivers/base" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" +) + +func (d *OpenListShare) request(api, method string, callback base.ReqCallback) ([]byte, int, error) { + url := d.Address + "/api" + api + req := base.RestyClient.R() + if callback != nil { + callback(req) + } + res, err := req.Execute(method, url) + if err != nil { + code := 0 + if res != nil { + code = res.StatusCode() + } + return nil, code, err + } + if res.StatusCode() >= 400 { + return nil, res.StatusCode(), fmt.Errorf("request failed, status: %s", res.Status()) + } + code := utils.Json.Get(res.Body(), "code").ToInt() + if code != 200 { + return nil, code, fmt.Errorf("request failed, code: %d, message: %s", code, utils.Json.Get(res.Body(), "message").ToString()) + } + return res.Body(), 200, nil +} diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index 4718b844..50e60420 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -111,6 +111,7 @@ func InitialSettings() []model.SettingItem { {Key: conf.Favicon, Value: "https://res.oplist.org/logo/logo.svg", MigrationValue: "https://cdn.oplist.org/gh/OpenListTeam/Logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE}, {Key: conf.MainColor, Value: "#1890ff", Type: conf.TypeString, Group: model.STYLE}, {Key: "home_icon", Value: "🏠", Type: conf.TypeString, Group: model.STYLE}, + {Key: "share_icon", Value: "🎁", Type: conf.TypeString, Group: model.STYLE}, {Key: "home_container", Value: "max_980px", Type: conf.TypeSelect, Options: "max_980px,hope_container", Group: model.STYLE}, {Key: "settings_layout", Value: "list", Type: conf.TypeSelect, Options: "list,responsive", Group: model.STYLE}, // preview settings @@ -163,6 +164,10 @@ func InitialSettings() []model.SettingItem { {Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL}, {Key: conf.IgnoreDirectLinkParams, Value: "sign,openlist_ts", Type: conf.TypeString, Group: model.GLOBAL}, {Key: conf.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC}, + {Key: conf.SharePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC}, + {Key: conf.ShareArchivePreview, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC}, + {Key: conf.ShareForceProxy, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE}, + {Key: conf.ShareSummaryContent, Value: "@{{creator}} shared {{#each files}}{{#if @first}}\"{{filename this}}\"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}", Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PUBLIC}, // single settings {Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}, diff --git a/internal/bootstrap/data/user.go b/internal/bootstrap/data/user.go index 00b89909..b1b1c77f 100644 --- a/internal/bootstrap/data/user.go +++ b/internal/bootstrap/data/user.go @@ -33,8 +33,8 @@ func initUser() { Role: model.ADMIN, BasePath: "/", Authn: "[]", - // 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives) - Permission: 0x31FF, + // 0(can see hidden) - 8(webdav read) & 12(can read archives) - 14(can share) + Permission: 0x71FF, } if err := op.CreateUser(admin); err != nil { panic(err) diff --git a/internal/conf/const.go b/internal/conf/const.go index 74a6cc31..fd0e1610 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -33,6 +33,7 @@ const ( PreviewArchivesByDefault = "preview_archives_by_default" ReadMeAutoRender = "readme_autorender" FilterReadMeScripts = "filter_readme_scripts" + // global HideFiles = "hide_files" CustomizeHead = "customize_head" @@ -45,6 +46,10 @@ const ( ForwardDirectLinkParams = "forward_direct_link_params" IgnoreDirectLinkParams = "ignore_direct_link_params" WebauthnLoginEnabled = "webauthn_login_enabled" + SharePreview = "share_preview" + ShareArchivePreview = "share_archive_preview" + ShareForceProxy = "share_force_proxy" + ShareSummaryContent = "share_summary_content" // index SearchIndex = "search_index" @@ -167,4 +172,5 @@ const ( RequestHeaderKey UserAgentKey PathKey + SharingIDKey ) diff --git a/internal/db/db.go b/internal/db/db.go index 2299a1a3..96529c15 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -12,7 +12,7 @@ var db *gorm.DB func Init(d *gorm.DB) { db = d - err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey)) + err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB)) if err != nil { log.Fatalf("failed migrate database: %s", err.Error()) } diff --git a/internal/db/sharing.go b/internal/db/sharing.go new file mode 100644 index 00000000..3748796b --- /dev/null +++ b/internal/db/sharing.go @@ -0,0 +1,62 @@ +package db + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/utils/random" + "github.com/pkg/errors" +) + +func GetSharingById(id string) (*model.SharingDB, error) { + s := model.SharingDB{ID: id} + if err := db.Where(s).First(&s).Error; err != nil { + return nil, errors.Wrapf(err, "failed get sharing") + } + return &s, nil +} + +func GetSharings(pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) { + sharingDB := db.Model(&model.SharingDB{}) + if err := sharingDB.Count(&count).Error; err != nil { + return nil, 0, errors.Wrapf(err, "failed get sharings count") + } + if err := sharingDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&sharings).Error; err != nil { + return nil, 0, errors.Wrapf(err, "failed get find sharings") + } + return sharings, count, nil +} + +func GetSharingsByCreatorId(creator uint, pageIndex, pageSize int) (sharings []model.SharingDB, count int64, err error) { + sharingDB := db.Model(&model.SharingDB{}) + cond := model.SharingDB{CreatorId: creator} + if err := sharingDB.Where(cond).Count(&count).Error; err != nil { + return nil, 0, errors.Wrapf(err, "failed get sharings count") + } + if err := sharingDB.Where(cond).Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&sharings).Error; err != nil { + return nil, 0, errors.Wrapf(err, "failed get find sharings") + } + return sharings, count, nil +} + +func CreateSharing(s *model.SharingDB) (string, error) { + id := random.String(8) + for len(id) < 12 { + old := model.SharingDB{ + ID: id, + } + if err := db.Where(old).First(&old).Error; err != nil { + s.ID = id + return id, errors.WithStack(db.Create(s).Error) + } + id += random.String(1) + } + return "", errors.New("failed find valid id") +} + +func UpdateSharing(s *model.SharingDB) error { + return errors.WithStack(db.Save(s).Error) +} + +func DeleteSharingById(id string) error { + s := model.SharingDB{ID: id} + return errors.WithStack(db.Where(s).Delete(&s).Error) +} diff --git a/internal/errs/errors.go b/internal/errs/errors.go index df809691..4910094e 100644 --- a/internal/errs/errors.go +++ b/internal/errs/errors.go @@ -23,6 +23,10 @@ var ( UnknownArchiveFormat = errors.New("unknown archive format") WrongArchivePassword = errors.New("wrong archive password") DriverExtractNotSupported = errors.New("driver extraction not supported") + + WrongShareCode = errors.New("wrong share code") + InvalidSharing = errors.New("invalid sharing") + SharingNotFound = errors.New("sharing not found") ) // NewErr wrap constant error with an extra message diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 2f23ba71..8c1f646b 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -168,7 +168,7 @@ func GetStorage(path string, args *GetStoragesArgs) (driver.Driver, error) { func Other(ctx context.Context, args model.FsOtherArgs) (interface{}, error) { res, err := other(ctx, args) if err != nil { - log.Errorf("failed remove %s: %+v", args.Path, err) + log.Errorf("failed get other %s: %+v", args.Path, err) } return res, err } diff --git a/internal/model/args.go b/internal/model/args.go index e655882a..ead19cbb 100644 --- a/internal/model/args.go +++ b/internal/model/args.go @@ -77,6 +77,26 @@ type ArchiveDecompressArgs struct { PutIntoNewDir bool } +type SharingListArgs struct { + Refresh bool + Pwd string +} + +type SharingArchiveMetaArgs struct { + ArchiveMetaArgs + Pwd string +} + +type SharingArchiveListArgs struct { + ArchiveListArgs + Pwd string +} + +type SharingLinkArgs struct { + Pwd string + LinkArgs +} + type RangeReaderIF interface { RangeRead(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) } diff --git a/internal/model/sharing.go b/internal/model/sharing.go new file mode 100644 index 00000000..dfbc81b0 --- /dev/null +++ b/internal/model/sharing.go @@ -0,0 +1,47 @@ +package model + +import "time" + +type SharingDB struct { + ID string `json:"id" gorm:"type:char(12);primaryKey"` + FilesRaw string `json:"-" gorm:"type:text"` + Expires *time.Time `json:"expires"` + Pwd string `json:"pwd"` + Accessed int `json:"accessed"` + MaxAccessed int `json:"max_accessed"` + CreatorId uint `json:"-"` + Disabled bool `json:"disabled"` + Remark string `json:"remark"` + Readme string `json:"readme" gorm:"type:text"` + Header string `json:"header" gorm:"type:text"` + Sort +} + +type Sharing struct { + *SharingDB + Files []string `json:"files"` + Creator *User `json:"-"` +} + +func (s *Sharing) Valid() bool { + if s.Disabled { + return false + } + if s.MaxAccessed > 0 && s.Accessed >= s.MaxAccessed { + return false + } + if len(s.Files) == 0 { + return false + } + if !s.Creator.CanShare() { + return false + } + if s.Expires != nil && !s.Expires.IsZero() && s.Expires.Before(time.Now()) { + return false + } + return true +} + +func (s *Sharing) Verify(pwd string) bool { + return s.Pwd == "" || s.Pwd == pwd +} diff --git a/internal/model/user.go b/internal/model/user.go index 78fa33b6..ae2d7d58 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -54,6 +54,7 @@ type User struct { // 11: ftp/sftp write // 12: can read archives // 13: can decompress archives + // 14: can share Permission int32 `json:"permission"` OtpSecret string `json:"-"` SsoID string `json:"sso_id"` // unique by sso platform @@ -145,6 +146,10 @@ func (u *User) CanDecompress() bool { return (u.Permission>>13)&1 == 1 } +func (u *User) CanShare() bool { + return (u.Permission>>14)&1 == 1 +} + func (u *User) JoinPath(reqPath string) (string, error) { return utils.JoinBasePath(u.BasePath, reqPath) } diff --git a/internal/op/sharing.go b/internal/op/sharing.go new file mode 100644 index 00000000..dbff1ba8 --- /dev/null +++ b/internal/op/sharing.go @@ -0,0 +1,139 @@ +package op + +import ( + "fmt" + stdpath "path" + "strings" + + "github.com/OpenListTeam/OpenList/v4/internal/db" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/singleflight" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/go-cache" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func makeJoined(sdb []model.SharingDB) []model.Sharing { + creator := make(map[uint]*model.User) + return utils.MustSliceConvert(sdb, func(s model.SharingDB) model.Sharing { + var c *model.User + var ok bool + if c, ok = creator[s.CreatorId]; !ok { + var err error + if c, err = GetUserById(s.CreatorId); err != nil { + c = nil + } else { + creator[s.CreatorId] = c + } + } + var files []string + if err := utils.Json.UnmarshalFromString(s.FilesRaw, &files); err != nil { + files = make([]string, 0) + } + return model.Sharing{ + SharingDB: &s, + Files: files, + Creator: c, + } + }) +} + +var sharingCache = cache.NewMemCache(cache.WithShards[*model.Sharing](8)) +var sharingG singleflight.Group[*model.Sharing] + +func GetSharingById(id string, refresh ...bool) (*model.Sharing, error) { + if !utils.IsBool(refresh...) { + if sharing, ok := sharingCache.Get(id); ok { + log.Debugf("use cache when get sharing %s", id) + return sharing, nil + } + } + sharing, err, _ := sharingG.Do(id, func() (*model.Sharing, error) { + s, err := db.GetSharingById(id) + if err != nil { + return nil, errors.WithMessagef(err, "failed get sharing [%s]", id) + } + creator, err := GetUserById(s.CreatorId) + if err != nil { + return nil, errors.WithMessagef(err, "failed get sharing creator [%s]", id) + } + var files []string + if err = utils.Json.UnmarshalFromString(s.FilesRaw, &files); err != nil { + files = make([]string, 0) + } + return &model.Sharing{ + SharingDB: s, + Files: files, + Creator: creator, + }, nil + }) + return sharing, err +} + +func GetSharings(pageIndex, pageSize int) ([]model.Sharing, int64, error) { + s, cnt, err := db.GetSharings(pageIndex, pageSize) + if err != nil { + return nil, 0, errors.WithStack(err) + } + return makeJoined(s), cnt, nil +} + +func GetSharingsByCreatorId(userId uint, pageIndex, pageSize int) ([]model.Sharing, int64, error) { + s, cnt, err := db.GetSharingsByCreatorId(userId, pageIndex, pageSize) + if err != nil { + return nil, 0, errors.WithStack(err) + } + return makeJoined(s), cnt, nil +} + +func GetSharingUnwrapPath(sharing *model.Sharing, path string) (unwrapPath string, err error) { + if len(sharing.Files) == 0 { + return "", errors.New("cannot get actual path of an invalid sharing") + } + if len(sharing.Files) == 1 { + return stdpath.Join(sharing.Files[0], path), nil + } + path = utils.FixAndCleanPath(path)[1:] + if len(path) == 0 { + return "", errors.New("cannot get actual path of a sharing root path") + } + mapPath := "" + child, rest, _ := strings.Cut(path, "/") + for _, c := range sharing.Files { + if child == stdpath.Base(c) { + mapPath = c + break + } + } + if mapPath == "" { + return "", fmt.Errorf("failed find child [%s] of sharing [%s]", child, sharing.ID) + } + return stdpath.Join(mapPath, rest), nil +} + +func CreateSharing(sharing *model.Sharing) (id string, err error) { + sharing.CreatorId = sharing.Creator.ID + sharing.FilesRaw, err = utils.Json.MarshalToString(utils.MustSliceConvert(sharing.Files, utils.FixAndCleanPath)) + if err != nil { + return "", errors.WithStack(err) + } + return db.CreateSharing(sharing.SharingDB) +} + +func UpdateSharing(sharing *model.Sharing, skipMarshal ...bool) (err error) { + if !utils.IsBool(skipMarshal...) { + sharing.CreatorId = sharing.Creator.ID + sharing.FilesRaw, err = utils.Json.MarshalToString(utils.MustSliceConvert(sharing.Files, utils.FixAndCleanPath)) + if err != nil { + return errors.WithStack(err) + } + } + sharingCache.Del(sharing.ID) + return db.UpdateSharing(sharing.SharingDB) +} + +func DeleteSharing(sid string) error { + sharingCache.Del(sid) + return db.DeleteSharingById(sid) +} diff --git a/internal/sharing/archive.go b/internal/sharing/archive.go new file mode 100644 index 00000000..ddfabd8c --- /dev/null +++ b/internal/sharing/archive.go @@ -0,0 +1,65 @@ +package sharing + +import ( + "context" + + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/pkg/errors" +) + +func archiveMeta(ctx context.Context, sid, path string, args model.SharingArchiveMetaArgs) (*model.Sharing, *model.ArchiveMetaProvider, error) { + sharing, err := op.GetSharingById(sid, args.Refresh) + if err != nil { + return nil, nil, errors.WithStack(errs.SharingNotFound) + } + if !sharing.Valid() { + return sharing, nil, errors.WithStack(errs.InvalidSharing) + } + if !sharing.Verify(args.Pwd) { + return sharing, nil, errors.WithStack(errs.WrongShareCode) + } + path = utils.FixAndCleanPath(path) + if len(sharing.Files) == 1 || path != "/" { + unwrapPath, err := op.GetSharingUnwrapPath(sharing, path) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing unwrap path") + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing file") + } + obj, err := op.GetArchiveMeta(ctx, storage, actualPath, args.ArchiveMetaArgs) + return sharing, obj, err + } + return nil, nil, errors.New("cannot get sharing root archive meta") +} + +func archiveList(ctx context.Context, sid, path string, args model.SharingArchiveListArgs) (*model.Sharing, []model.Obj, error) { + sharing, err := op.GetSharingById(sid, args.Refresh) + if err != nil { + return nil, nil, errors.WithStack(errs.SharingNotFound) + } + if !sharing.Valid() { + return sharing, nil, errors.WithStack(errs.InvalidSharing) + } + if !sharing.Verify(args.Pwd) { + return sharing, nil, errors.WithStack(errs.WrongShareCode) + } + path = utils.FixAndCleanPath(path) + if len(sharing.Files) == 1 || path != "/" { + unwrapPath, err := op.GetSharingUnwrapPath(sharing, path) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing unwrap path") + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing file") + } + obj, err := op.ListArchive(ctx, storage, actualPath, args.ArchiveListArgs) + return sharing, obj, err + } + return nil, nil, errors.New("cannot get sharing root archive list") +} diff --git a/internal/sharing/get.go b/internal/sharing/get.go new file mode 100644 index 00000000..28815ad3 --- /dev/null +++ b/internal/sharing/get.go @@ -0,0 +1,60 @@ +package sharing + +import ( + "context" + stdpath "path" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/pkg/errors" +) + +func get(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, model.Obj, error) { + sharing, err := op.GetSharingById(sid, args.Refresh) + if err != nil { + return nil, nil, errors.WithStack(errs.SharingNotFound) + } + if !sharing.Valid() { + return sharing, nil, errors.WithStack(errs.InvalidSharing) + } + if !sharing.Verify(args.Pwd) { + return sharing, nil, errors.WithStack(errs.WrongShareCode) + } + path = utils.FixAndCleanPath(path) + if len(sharing.Files) == 1 || path != "/" { + unwrapPath, err := op.GetSharingUnwrapPath(sharing, path) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing unwrap path") + } + if unwrapPath != "/" { + virtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(unwrapPath)) + for _, f := range virtualFiles { + if f.GetName() == stdpath.Base(unwrapPath) { + return sharing, f, nil + } + } + } else { + return sharing, &model.Object{ + Name: sid, + Size: 0, + Modified: time.Time{}, + IsFolder: true, + }, nil + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing file") + } + obj, err := op.Get(ctx, storage, actualPath) + return sharing, obj, err + } + return sharing, &model.Object{ + Name: sid, + Size: 0, + Modified: time.Time{}, + IsFolder: true, + }, nil +} diff --git a/internal/sharing/link.go b/internal/sharing/link.go new file mode 100644 index 00000000..32ae6b83 --- /dev/null +++ b/internal/sharing/link.go @@ -0,0 +1,46 @@ +package sharing + +import ( + "context" + "strings" + + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/pkg/errors" +) + +func link(ctx context.Context, sid, path string, args *LinkArgs) (*model.Sharing, *model.Link, model.Obj, error) { + sharing, err := op.GetSharingById(sid, args.SharingListArgs.Refresh) + if err != nil { + return nil, nil, nil, errors.WithStack(errs.SharingNotFound) + } + if !sharing.Valid() { + return sharing, nil, nil, errors.WithStack(errs.InvalidSharing) + } + if !sharing.Verify(args.Pwd) { + return sharing, nil, nil, errors.WithStack(errs.WrongShareCode) + } + path = utils.FixAndCleanPath(path) + if len(sharing.Files) == 1 || path != "/" { + unwrapPath, err := op.GetSharingUnwrapPath(sharing, path) + if err != nil { + return nil, nil, nil, errors.WithMessage(err, "failed get sharing unwrap path") + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if err != nil { + return nil, nil, nil, errors.WithMessage(err, "failed get sharing link") + } + l, obj, err := op.Link(ctx, storage, actualPath, args.LinkArgs) + if err != nil { + return nil, nil, nil, errors.WithMessage(err, "failed get sharing link") + } + if l.URL != "" && !strings.HasPrefix(l.URL, "http://") && !strings.HasPrefix(l.URL, "https://") { + l.URL = common.GetApiUrl(ctx) + l.URL + } + return sharing, l, obj, nil + } + return nil, nil, nil, errors.New("cannot get sharing root link") +} diff --git a/internal/sharing/list.go b/internal/sharing/list.go new file mode 100644 index 00000000..0600e8a8 --- /dev/null +++ b/internal/sharing/list.go @@ -0,0 +1,83 @@ +package sharing + +import ( + "context" + stdpath "path" + + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/pkg/errors" +) + +func list(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, []model.Obj, error) { + sharing, err := op.GetSharingById(sid, args.Refresh) + if err != nil { + return nil, nil, errors.WithStack(errs.SharingNotFound) + } + if !sharing.Valid() { + return sharing, nil, errors.WithStack(errs.InvalidSharing) + } + if !sharing.Verify(args.Pwd) { + return sharing, nil, errors.WithStack(errs.WrongShareCode) + } + path = utils.FixAndCleanPath(path) + if len(sharing.Files) == 1 || path != "/" { + unwrapPath, err := op.GetSharingUnwrapPath(sharing, path) + if err != nil { + return nil, nil, errors.WithMessage(err, "failed get sharing unwrap path") + } + virtualFiles := op.GetStorageVirtualFilesByPath(unwrapPath) + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if err != nil && len(virtualFiles) == 0 { + return nil, nil, errors.WithMessage(err, "failed list sharing") + } + var objs []model.Obj + if storage != nil { + objs, err = op.List(ctx, storage, actualPath, model.ListArgs{ + Refresh: args.Refresh, + ReqPath: stdpath.Join(sid, path), + }) + if err != nil && len(virtualFiles) == 0 { + return nil, nil, errors.WithMessage(err, "failed list sharing") + } + } + om := model.NewObjMerge() + objs = om.Merge(objs, virtualFiles...) + model.SortFiles(objs, sharing.OrderBy, sharing.OrderDirection) + model.ExtractFolder(objs, sharing.ExtractFolder) + return sharing, objs, nil + } + objs := make([]model.Obj, 0, len(sharing.Files)) + for _, f := range sharing.Files { + if f != "/" { + isVf := false + virtualFiles := op.GetStorageVirtualFilesByPath(stdpath.Dir(f)) + for _, vf := range virtualFiles { + if vf.GetName() == stdpath.Base(f) { + objs = append(objs, vf) + isVf = true + break + } + } + if isVf { + continue + } + } else { + continue + } + storage, actualPath, err := op.GetStorageAndActualPath(f) + if err != nil { + continue + } + obj, err := op.Get(ctx, storage, actualPath) + if err != nil { + continue + } + objs = append(objs, obj) + } + model.SortFiles(objs, sharing.OrderBy, sharing.OrderDirection) + model.ExtractFolder(objs, sharing.ExtractFolder) + return sharing, objs, nil +} diff --git a/internal/sharing/sharing.go b/internal/sharing/sharing.go new file mode 100644 index 00000000..40a2a3a3 --- /dev/null +++ b/internal/sharing/sharing.go @@ -0,0 +1,58 @@ +package sharing + +import ( + "context" + + "github.com/OpenListTeam/OpenList/v4/internal/model" + log "github.com/sirupsen/logrus" +) + +func List(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, []model.Obj, error) { + sharing, res, err := list(ctx, sid, path, args) + if err != nil { + log.Errorf("failed list sharing %s/%s: %+v", sid, path, err) + return nil, nil, err + } + return sharing, res, nil +} + +func Get(ctx context.Context, sid, path string, args model.SharingListArgs) (*model.Sharing, model.Obj, error) { + sharing, res, err := get(ctx, sid, path, args) + if err != nil { + log.Warnf("failed get sharing %s/%s: %s", sid, path, err) + return nil, nil, err + } + return sharing, res, nil +} + +func ArchiveMeta(ctx context.Context, sid, path string, args model.SharingArchiveMetaArgs) (*model.Sharing, *model.ArchiveMetaProvider, error) { + sharing, res, err := archiveMeta(ctx, sid, path, args) + if err != nil { + log.Warnf("failed get sharing archive meta %s/%s: %s", sid, path, err) + return nil, nil, err + } + return sharing, res, nil +} + +func ArchiveList(ctx context.Context, sid, path string, args model.SharingArchiveListArgs) (*model.Sharing, []model.Obj, error) { + sharing, res, err := archiveList(ctx, sid, path, args) + if err != nil { + log.Warnf("failed list sharing archive %s/%s: %s", sid, path, err) + return nil, nil, err + } + return sharing, res, nil +} + +type LinkArgs struct { + model.SharingListArgs + model.LinkArgs +} + +func Link(ctx context.Context, sid, path string, args *LinkArgs) (*model.Sharing, *model.Link, model.Obj, error) { + sharing, res, file, err := link(ctx, sid, path, args) + if err != nil { + log.Errorf("failed get sharing link %s/%s: %+v", sid, path, err) + return nil, nil, nil, err + } + return sharing, res, file, nil +} diff --git a/server/handles/archive.go b/server/handles/archive.go index c10cca24..f8804235 100644 --- a/server/handles/archive.go +++ b/server/handles/archive.go @@ -3,9 +3,9 @@ package handles import ( "encoding/json" "fmt" + "io" stdpath "path" - - "github.com/OpenListTeam/OpenList/v4/internal/task" + "strings" "github.com/OpenListTeam/OpenList/v4/internal/archive/tool" "github.com/OpenListTeam/OpenList/v4/internal/conf" @@ -15,6 +15,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/setting" "github.com/OpenListTeam/OpenList/v4/internal/sign" + "github.com/OpenListTeam/OpenList/v4/internal/task" "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/OpenListTeam/OpenList/v4/server/common" "github.com/gin-gonic/gin" @@ -71,13 +72,26 @@ func toContentResp(objs []model.ObjTree) []ArchiveContentResp { return ret } -func FsArchiveMeta(c *gin.Context) { +func FsArchiveMetaSplit(c *gin.Context) { var req ArchiveMetaReq if err := c.ShouldBind(&req); err != nil { common.ErrorResp(c, err, 400) return } + if strings.HasPrefix(req.Path, "/@s") { + req.Path = strings.TrimPrefix(req.Path, "/@s") + SharingArchiveMeta(c, &req) + return + } user := c.Request.Context().Value(conf.UserKey).(*model.User) + if user.IsGuest() && user.Disabled { + common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + return + } + FsArchiveMeta(c, &req, user) +} + +func FsArchiveMeta(c *gin.Context, req *ArchiveMetaReq, user *model.User) { if !user.CanReadArchives() { common.ErrorResp(c, errs.PermissionDenied, 403) return @@ -142,19 +156,27 @@ type ArchiveListReq struct { InnerPath string `json:"inner_path" form:"inner_path"` } -type ArchiveListResp struct { - Content []ObjResp `json:"content"` - Total int64 `json:"total"` -} - -func FsArchiveList(c *gin.Context) { +func FsArchiveListSplit(c *gin.Context) { var req ArchiveListReq if err := c.ShouldBind(&req); err != nil { common.ErrorResp(c, err, 400) return } req.Validate() + if strings.HasPrefix(req.Path, "/@s") { + req.Path = strings.TrimPrefix(req.Path, "/@s") + SharingArchiveList(c, &req) + return + } user := c.Request.Context().Value(conf.UserKey).(*model.User) + if user.IsGuest() && user.Disabled { + common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + return + } + FsArchiveList(c, &req, user) +} + +func FsArchiveList(c *gin.Context, req *ArchiveListReq, user *model.User) { if !user.CanReadArchives() { common.ErrorResp(c, errs.PermissionDenied, 403) return @@ -201,7 +223,7 @@ func FsArchiveList(c *gin.Context) { ret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) { return toObjsRespWithoutSignAndThumb(src), nil }) - common.SuccessResp(c, ArchiveListResp{ + common.SuccessResp(c, common.PageResp{ Content: ret, Total: int64(total), }) @@ -358,6 +380,24 @@ func ArchiveProxy(c *gin.Context) { } } +func proxyInternalExtract(c *gin.Context, rc io.ReadCloser, size int64, fileName string) { + defer func() { + if err := rc.Close(); err != nil { + log.Errorf("failed to close file streamer, %v", err) + } + }() + headers := map[string]string{ + "Referrer-Policy": "no-referrer", + "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", + } + headers["Content-Disposition"] = utils.GenerateContentDisposition(fileName) + contentType := c.Request.Header.Get("Content-Type") + if contentType == "" { + contentType = utils.GetMimeType(fileName) + } + c.DataFromReader(200, size, contentType, rc, headers) +} + func ArchiveInternalExtract(c *gin.Context) { archiveRawPath := c.Request.Context().Value(conf.PathKey).(string) innerPath := utils.FixAndCleanPath(c.Query("inner")) @@ -376,22 +416,8 @@ func ArchiveInternalExtract(c *gin.Context) { common.ErrorResp(c, err, 500) return } - defer func() { - if err := rc.Close(); err != nil { - log.Errorf("failed to close file streamer, %v", err) - } - }() - headers := map[string]string{ - "Referrer-Policy": "no-referrer", - "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", - } fileName := stdpath.Base(innerPath) - headers["Content-Disposition"] = utils.GenerateContentDisposition(fileName) - contentType := c.Request.Header.Get("Content-Type") - if contentType == "" { - contentType = utils.GetMimeType(fileName) - } - c.DataFromReader(200, size, contentType, rc, headers) + proxyInternalExtract(c, rc, size, fileName) } func ArchiveExtensions(c *gin.Context) { diff --git a/server/handles/fsread.go b/server/handles/fsread.go index 2cabada5..b9a4f09d 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -56,14 +56,27 @@ type FsListResp struct { Provider string `json:"provider"` } -func FsList(c *gin.Context) { +func FsListSplit(c *gin.Context) { var req ListReq if err := c.ShouldBind(&req); err != nil { common.ErrorResp(c, err, 400) return } req.Validate() + if strings.HasPrefix(req.Path, "/@s") { + req.Path = strings.TrimPrefix(req.Path, "/@s") + SharingList(c, &req) + return + } user := c.Request.Context().Value(conf.UserKey).(*model.User) + if user.IsGuest() && user.Disabled { + common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + return + } + FsList(c, &req, user) +} + +func FsList(c *gin.Context, req *ListReq, user *model.User) { reqPath, err := user.JoinPath(req.Path) if err != nil { common.ErrorResp(c, err, 403) @@ -243,13 +256,26 @@ type FsGetResp struct { Related []ObjResp `json:"related"` } -func FsGet(c *gin.Context) { +func FsGetSplit(c *gin.Context) { var req FsGetReq if err := c.ShouldBind(&req); err != nil { common.ErrorResp(c, err, 400) return } + if strings.HasPrefix(req.Path, "/@s") { + req.Path = strings.TrimPrefix(req.Path, "/@s") + SharingGet(c, &req) + return + } user := c.Request.Context().Value(conf.UserKey).(*model.User) + if user.IsGuest() && user.Disabled { + common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + return + } + FsGet(c, &req, user) +} + +func FsGet(c *gin.Context, req *FsGetReq, user *model.User) { reqPath, err := user.JoinPath(req.Path) if err != nil { common.ErrorResp(c, err, 403) diff --git a/server/handles/sharing.go b/server/handles/sharing.go new file mode 100644 index 00000000..c1d1c15a --- /dev/null +++ b/server/handles/sharing.go @@ -0,0 +1,551 @@ +package handles + +import ( + "fmt" + stdpath "path" + "strings" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/internal/setting" + "github.com/OpenListTeam/OpenList/v4/internal/sharing" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/OpenListTeam/go-cache" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +func SharingGet(c *gin.Context, req *FsGetReq) { + sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/") + if sid == "" { + common.ErrorStrResp(c, "invalid share id", 400) + return + } + s, obj, err := sharing.Get(c.Request.Context(), sid, path, model.SharingListArgs{ + Refresh: false, + Pwd: req.Password, + }) + if dealError(c, err) { + return + } + _ = countAccess(c.ClientIP(), s) + fakePath := fmt.Sprintf("/%s/%s", sid, path) + url := "" + if !obj.IsDir() { + url = fmt.Sprintf("%s/sd%s", common.GetApiUrl(c), utils.EncodePath(fakePath, true)) + if s.Pwd != "" { + url += "?pwd=" + s.Pwd + } + } + thumb, _ := model.GetThumb(obj) + common.SuccessResp(c, FsGetResp{ + ObjResp: ObjResp{ + Id: "", + Path: fakePath, + Name: obj.GetName(), + Size: obj.GetSize(), + IsDir: obj.IsDir(), + Modified: obj.ModTime(), + Created: obj.CreateTime(), + HashInfoStr: obj.GetHash().String(), + HashInfo: obj.GetHash().Export(), + Sign: "", + Type: utils.GetFileType(obj.GetName()), + Thumb: thumb, + }, + RawURL: url, + Readme: s.Readme, + Header: s.Header, + Provider: "unknown", + Related: nil, + }) +} + +func SharingList(c *gin.Context, req *ListReq) { + sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/") + if sid == "" { + common.ErrorStrResp(c, "invalid share id", 400) + return + } + s, objs, err := sharing.List(c.Request.Context(), sid, path, model.SharingListArgs{ + Refresh: req.Refresh, + Pwd: req.Password, + }) + if dealError(c, err) { + return + } + _ = countAccess(c.ClientIP(), s) + fakePath := fmt.Sprintf("/%s/%s", sid, path) + total, objs := pagination(objs, &req.PageReq) + common.SuccessResp(c, FsListResp{ + Content: utils.MustSliceConvert(objs, func(obj model.Obj) ObjResp { + thumb, _ := model.GetThumb(obj) + return ObjResp{ + Id: "", + Path: stdpath.Join(fakePath, obj.GetName()), + Name: obj.GetName(), + Size: obj.GetSize(), + IsDir: obj.IsDir(), + Modified: obj.ModTime(), + Created: obj.CreateTime(), + HashInfoStr: obj.GetHash().String(), + HashInfo: obj.GetHash().Export(), + Sign: "", + Thumb: thumb, + Type: utils.GetObjType(obj.GetName(), obj.IsDir()), + } + }), + Total: int64(total), + Readme: s.Readme, + Header: s.Header, + Write: false, + Provider: "unknown", + }) +} + +func SharingArchiveMeta(c *gin.Context, req *ArchiveMetaReq) { + if !setting.GetBool(conf.ShareArchivePreview) { + common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403) + return + } + sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/") + if sid == "" { + common.ErrorStrResp(c, "invalid share id", 400) + return + } + archiveArgs := model.ArchiveArgs{ + LinkArgs: model.LinkArgs{ + Header: c.Request.Header, + Type: c.Query("type"), + }, + Password: req.ArchivePass, + } + s, ret, err := sharing.ArchiveMeta(c.Request.Context(), sid, path, model.SharingArchiveMetaArgs{ + ArchiveMetaArgs: model.ArchiveMetaArgs{ + ArchiveArgs: archiveArgs, + Refresh: req.Refresh, + }, + Pwd: req.Password, + }) + if dealError(c, err) { + return + } + _ = countAccess(c.ClientIP(), s) + fakePath := fmt.Sprintf("/%s/%s", sid, path) + url := fmt.Sprintf("%s/sad%s", common.GetApiUrl(c), utils.EncodePath(fakePath, true)) + if s.Pwd != "" { + url += "?pwd=" + s.Pwd + } + common.SuccessResp(c, ArchiveMetaResp{ + Comment: ret.GetComment(), + IsEncrypted: ret.IsEncrypted(), + Content: toContentResp(ret.GetTree()), + Sort: ret.Sort, + RawURL: url, + Sign: "", + }) +} + +func SharingArchiveList(c *gin.Context, req *ArchiveListReq) { + if !setting.GetBool(conf.ShareArchivePreview) { + common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403) + return + } + sid, path, _ := strings.Cut(strings.TrimPrefix(req.Path, "/"), "/") + if sid == "" { + common.ErrorStrResp(c, "invalid share id", 400) + return + } + innerArgs := model.ArchiveInnerArgs{ + ArchiveArgs: model.ArchiveArgs{ + LinkArgs: model.LinkArgs{ + Header: c.Request.Header, + Type: c.Query("type"), + }, + Password: req.ArchivePass, + }, + InnerPath: utils.FixAndCleanPath(req.InnerPath), + } + s, objs, err := sharing.ArchiveList(c.Request.Context(), sid, path, model.SharingArchiveListArgs{ + ArchiveListArgs: model.ArchiveListArgs{ + ArchiveInnerArgs: innerArgs, + Refresh: req.Refresh, + }, + Pwd: req.Password, + }) + if dealError(c, err) { + return + } + _ = countAccess(c.ClientIP(), s) + total, objs := pagination(objs, &req.PageReq) + ret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) { + return toObjsRespWithoutSignAndThumb(src), nil + }) + common.SuccessResp(c, common.PageResp{ + Content: ret, + Total: int64(total), + }) +} + +func SharingDown(c *gin.Context) { + sid := c.Request.Context().Value(conf.SharingIDKey).(string) + path := c.Request.Context().Value(conf.PathKey).(string) + pwd := c.Query("pwd") + s, err := op.GetSharingById(sid) + if err == nil { + if !s.Valid() { + err = errs.InvalidSharing + } else if !s.Verify(pwd) { + err = errs.WrongShareCode + } else if len(s.Files) != 1 && path == "/" { + err = errors.New("cannot get sharing root link") + } + } + if dealError(c, err) { + return + } + unwrapPath, err := op.GetSharingUnwrapPath(s, path) + if err != nil { + common.ErrorStrResp(c, "failed get sharing unwrap path", 500) + return + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if dealError(c, err) { + return + } + if setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) { + link, obj, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{ + Header: c.Request.Header, + Type: c.Query("type"), + }) + if err != nil { + common.ErrorResp(c, errors.WithMessage(err, "failed get sharing link"), 500) + return + } + _ = countAccess(c.ClientIP(), s) + proxy(c, link, obj, storage.GetStorage().ProxyRange) + } else { + link, _, err := op.Link(c.Request.Context(), storage, actualPath, model.LinkArgs{ + IP: c.ClientIP(), + Header: c.Request.Header, + Type: c.Query("type"), + Redirect: true, + }) + if err != nil { + common.ErrorResp(c, errors.WithMessage(err, "failed get sharing link"), 500) + return + } + _ = countAccess(c.ClientIP(), s) + redirect(c, link) + } +} + +func SharingArchiveExtract(c *gin.Context) { + if !setting.GetBool(conf.ShareArchivePreview) { + common.ErrorStrResp(c, "sharing archives previewing is not allowed", 403) + return + } + sid := c.Request.Context().Value(conf.SharingIDKey).(string) + path := c.Request.Context().Value(conf.PathKey).(string) + pwd := c.Query("pwd") + innerPath := utils.FixAndCleanPath(c.Query("inner")) + archivePass := c.Query("pass") + s, err := op.GetSharingById(sid) + if err == nil { + if !s.Valid() { + err = errs.InvalidSharing + } else if !s.Verify(pwd) { + err = errs.WrongShareCode + } else if len(s.Files) != 1 && path == "/" { + err = errors.New("cannot extract sharing root") + } + } + if dealError(c, err) { + return + } + unwrapPath, err := op.GetSharingUnwrapPath(s, path) + if err != nil { + common.ErrorStrResp(c, "failed get sharing unwrap path", 500) + return + } + storage, actualPath, err := op.GetStorageAndActualPath(unwrapPath) + if dealError(c, err) { + return + } + args := model.ArchiveInnerArgs{ + ArchiveArgs: model.ArchiveArgs{ + LinkArgs: model.LinkArgs{ + Header: c.Request.Header, + Type: c.Query("type"), + }, + Password: archivePass, + }, + InnerPath: innerPath, + } + if _, ok := storage.(driver.ArchiveReader); ok { + if setting.GetBool(conf.ShareForceProxy) || common.ShouldProxy(storage, stdpath.Base(actualPath)) { + link, obj, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args) + if dealError(c, err) { + return + } + proxy(c, link, obj, storage.GetStorage().ProxyRange) + } else { + args.Redirect = true + link, _, err := op.DriverExtract(c.Request.Context(), storage, actualPath, args) + if dealError(c, err) { + return + } + redirect(c, link) + } + } else { + rc, size, err := op.InternalExtract(c.Request.Context(), storage, actualPath, args) + if dealError(c, err) { + return + } + fileName := stdpath.Base(innerPath) + proxyInternalExtract(c, rc, size, fileName) + } +} + +func dealError(c *gin.Context, err error) bool { + if err == nil { + return false + } else if errors.Is(err, errs.SharingNotFound) { + common.ErrorStrResp(c, "the share does not exist", 500) + } else if errors.Is(err, errs.InvalidSharing) { + common.ErrorStrResp(c, "the share has expired or is no longer valid", 500) + } else if errors.Is(err, errs.WrongShareCode) { + common.ErrorResp(c, err, 403) + } else if errors.Is(err, errs.WrongArchivePassword) { + common.ErrorResp(c, err, 202) + } else { + common.ErrorResp(c, err, 500) + } + return true +} + +type SharingResp struct { + *model.Sharing + CreatorName string `json:"creator"` + CreatorRole int `json:"creator_role"` +} + +func GetSharing(c *gin.Context) { + sid := c.Query("id") + user := c.Request.Context().Value(conf.UserKey).(*model.User) + s, err := op.GetSharingById(sid) + if err != nil || (!user.IsAdmin() && s.Creator.ID != user.ID) { + common.ErrorStrResp(c, "sharing not found", 404) + return + } + common.SuccessResp(c, SharingResp{ + Sharing: s, + CreatorName: s.Creator.Username, + CreatorRole: s.Creator.Role, + }) +} + +func ListSharings(c *gin.Context) { + var req model.PageReq + if err := c.ShouldBind(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + req.Validate() + user := c.Request.Context().Value(conf.UserKey).(*model.User) + var sharings []model.Sharing + var total int64 + var err error + if user.IsAdmin() { + sharings, total, err = op.GetSharings(req.Page, req.PerPage) + } else { + sharings, total, err = op.GetSharingsByCreatorId(user.ID, req.Page, req.PerPage) + } + if err != nil { + common.ErrorResp(c, err, 500, true) + return + } + common.SuccessResp(c, common.PageResp{ + Content: utils.MustSliceConvert(sharings, func(s model.Sharing) SharingResp { + return SharingResp{ + Sharing: &s, + CreatorName: s.Creator.Username, + CreatorRole: s.Creator.Role, + } + }), + Total: total, + }) +} + +type CreateSharingReq struct { + Files []string `json:"files"` + Expires *time.Time `json:"expires"` + Pwd string `json:"pwd"` + MaxAccessed int `json:"max_accessed"` + Disabled bool `json:"disabled"` + Remark string `json:"remark"` + Readme string `json:"readme"` + Header string `json:"header"` + model.Sort +} + +type UpdateSharingReq struct { + ID string `json:"id"` + Accessed int `json:"accessed"` + CreateSharingReq +} + +func UpdateSharing(c *gin.Context) { + var req UpdateSharingReq + if err := c.ShouldBind(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + if len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == "") { + common.ErrorStrResp(c, "must add at least 1 object", 400) + return + } + user := c.Request.Context().Value(conf.UserKey).(*model.User) + if !user.CanShare() { + common.ErrorStrResp(c, "permission denied", 403) + return + } + for i, s := range req.Files { + s = utils.FixAndCleanPath(s) + req.Files[i] = s + if !user.IsAdmin() && !strings.HasPrefix(s, user.BasePath) { + common.ErrorStrResp(c, fmt.Sprintf("permission denied to share path [%s]", s), 500) + return + } + } + s, err := op.GetSharingById(req.ID) + if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) { + common.ErrorStrResp(c, "sharing not found", 404) + return + } + s.Files = req.Files + s.Expires = req.Expires + s.Pwd = req.Pwd + s.Accessed = req.Accessed + s.MaxAccessed = req.MaxAccessed + s.Disabled = req.Disabled + s.Sort = req.Sort + s.Header = req.Header + s.Readme = req.Readme + s.Remark = req.Remark + if err = op.UpdateSharing(s); err != nil { + common.ErrorResp(c, err, 500) + } else { + common.SuccessResp(c, SharingResp{ + Sharing: s, + CreatorName: s.Creator.Username, + CreatorRole: s.Creator.Role, + }) + } +} + +func CreateSharing(c *gin.Context) { + var req CreateSharingReq + var err error + if err = c.ShouldBind(&req); err != nil { + common.ErrorResp(c, err, 400) + return + } + if len(req.Files) == 0 || (len(req.Files) == 1 && req.Files[0] == "") { + common.ErrorStrResp(c, "must add at least 1 object", 400) + return + } + user := c.Request.Context().Value(conf.UserKey).(*model.User) + if !user.CanShare() { + common.ErrorStrResp(c, "permission denied", 403) + return + } + for i, s := range req.Files { + s = utils.FixAndCleanPath(s) + req.Files[i] = s + if !user.IsAdmin() && !strings.HasPrefix(s, user.BasePath) { + common.ErrorStrResp(c, fmt.Sprintf("permission denied to share path [%s]", s), 500) + return + } + } + s := &model.Sharing{ + SharingDB: &model.SharingDB{ + Expires: req.Expires, + Pwd: req.Pwd, + Accessed: 0, + MaxAccessed: req.MaxAccessed, + Disabled: req.Disabled, + Sort: req.Sort, + Remark: req.Remark, + Readme: req.Readme, + Header: req.Header, + }, + Files: req.Files, + Creator: user, + } + var id string + if id, err = op.CreateSharing(s); err != nil { + common.ErrorResp(c, err, 500) + } else { + s.ID = id + common.SuccessResp(c, SharingResp{ + Sharing: s, + CreatorName: s.Creator.Username, + CreatorRole: s.Creator.Role, + }) + } +} + +func DeleteSharing(c *gin.Context) { + sid := c.Query("id") + user := c.Request.Context().Value(conf.UserKey).(*model.User) + s, err := op.GetSharingById(sid) + if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) { + common.ErrorResp(c, err, 404) + return + } + if err = op.DeleteSharing(sid); err != nil { + common.ErrorResp(c, err, 500) + } else { + common.SuccessResp(c) + } +} + +func SetEnableSharing(disable bool) func(ctx *gin.Context) { + return func(c *gin.Context) { + sid := c.Query("id") + user := c.Request.Context().Value(conf.UserKey).(*model.User) + s, err := op.GetSharingById(sid) + if err != nil || (!user.IsAdmin() && s.CreatorId != user.ID) { + common.ErrorStrResp(c, "sharing not found", 404) + return + } + s.Disabled = disable + if err = op.UpdateSharing(s, true); err != nil { + common.ErrorResp(c, err, 500) + } else { + common.SuccessResp(c) + } + } +} + +var ( + AccessCache = cache.NewMemCache[interface{}]() + AccessCountDelay = 30 * time.Minute +) + +func countAccess(ip string, s *model.Sharing) error { + key := fmt.Sprintf("%s:%s", s.ID, ip) + _, ok := AccessCache.Get(key) + if !ok { + AccessCache.Set(key, struct{}{}, cache.WithEx[interface{}](AccessCountDelay)) + s.Accessed += 1 + return op.UpdateSharing(s, true) + } + return nil +} diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 44a93140..0fc24361 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -14,63 +14,65 @@ import ( // Auth is a middleware that checks if the user is logged in. // if token is empty, set user to guest -func Auth(c *gin.Context) { - token := c.GetHeader("Authorization") - if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 { - admin, err := op.GetAdmin() +func Auth(allowDisabledGuest bool) func(c *gin.Context) { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 { + admin, err := op.GetAdmin() + if err != nil { + common.ErrorResp(c, err, 500) + c.Abort() + return + } + common.GinWithValue(c, conf.UserKey, admin) + log.Debugf("use admin token: %+v", admin) + c.Next() + return + } + if token == "" { + guest, err := op.GetGuest() + if err != nil { + common.ErrorResp(c, err, 500) + c.Abort() + return + } + if !allowDisabledGuest && guest.Disabled { + common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + c.Abort() + return + } + common.GinWithValue(c, conf.UserKey, guest) + log.Debugf("use empty token: %+v", guest) + c.Next() + return + } + userClaims, err := common.ParseToken(token) if err != nil { - common.ErrorResp(c, err, 500) + common.ErrorResp(c, err, 401) c.Abort() return } - common.GinWithValue(c, conf.UserKey, admin) - log.Debugf("use admin token: %+v", admin) - c.Next() - return - } - if token == "" { - guest, err := op.GetGuest() + user, err := op.GetUserByName(userClaims.Username) if err != nil { - common.ErrorResp(c, err, 500) + common.ErrorResp(c, err, 401) c.Abort() return } - if guest.Disabled { - common.ErrorStrResp(c, "Guest user is disabled, login please", 401) + // validate password timestamp + if userClaims.PwdTS != user.PwdTS { + common.ErrorStrResp(c, "Password has been changed, login please", 401) c.Abort() return } - common.GinWithValue(c, conf.UserKey, guest) - log.Debugf("use empty token: %+v", guest) + if user.Disabled { + common.ErrorStrResp(c, "Current user is disabled, replace please", 401) + c.Abort() + return + } + common.GinWithValue(c, conf.UserKey, user) + log.Debugf("use login token: %+v", user) c.Next() - return } - userClaims, err := common.ParseToken(token) - if err != nil { - common.ErrorResp(c, err, 401) - c.Abort() - return - } - user, err := op.GetUserByName(userClaims.Username) - if err != nil { - common.ErrorResp(c, err, 401) - c.Abort() - return - } - // validate password timestamp - if userClaims.PwdTS != user.PwdTS { - common.ErrorStrResp(c, "Password has been changed, login please", 401) - c.Abort() - return - } - if user.Disabled { - common.ErrorStrResp(c, "Current user is disabled, replace please", 401) - c.Abort() - return - } - common.GinWithValue(c, conf.UserKey, user) - log.Debugf("use login token: %+v", user) - c.Next() } func Authn(c *gin.Context) { diff --git a/server/middlewares/down.go b/server/middlewares/down.go index ee4815c2..2ede31ca 100644 --- a/server/middlewares/down.go +++ b/server/middlewares/down.go @@ -15,10 +15,15 @@ import ( "github.com/pkg/errors" ) +func PathParse(c *gin.Context) { + rawPath := parsePath(c.Param("path")) + common.GinWithValue(c, conf.PathKey, rawPath) + c.Next() +} + func Down(verifyFunc func(string, string) error) func(c *gin.Context) { return func(c *gin.Context) { - rawPath := parsePath(c.Param("path")) - common.GinWithValue(c, conf.PathKey, rawPath) + rawPath := c.Request.Context().Value(conf.PathKey).(string) meta, err := op.GetNearestMeta(rawPath) if err != nil { if !errors.Is(errors.Cause(err), errs.MetaNotFound) { diff --git a/server/middlewares/sharing.go b/server/middlewares/sharing.go new file mode 100644 index 00000000..d7549202 --- /dev/null +++ b/server/middlewares/sharing.go @@ -0,0 +1,18 @@ +package middlewares + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/gin-gonic/gin" +) + +func SharingIdParse(c *gin.Context) { + sid := c.Param("sid") + common.GinWithValue(c, conf.SharingIDKey, sid) + c.Next() +} + +func EmptyPathParse(c *gin.Context) { + common.GinWithValue(c, conf.PathKey, "/") + c.Next() +} diff --git a/server/router.go b/server/router.go index eaa83dde..66f0539b 100644 --- a/server/router.go +++ b/server/router.go @@ -44,20 +44,29 @@ func Init(e *gin.Engine) { downloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit) signCheck := middlewares.Down(sign.Verify) - g.GET("/d/*path", signCheck, downloadLimiter, handles.Down) - g.GET("/p/*path", signCheck, downloadLimiter, handles.Proxy) - g.HEAD("/d/*path", signCheck, handles.Down) - g.HEAD("/p/*path", signCheck, handles.Proxy) + g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down) + g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Proxy) + g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down) + g.HEAD("/p/*path", middlewares.PathParse, signCheck, handles.Proxy) archiveSignCheck := middlewares.Down(sign.VerifyArchive) - g.GET("/ad/*path", archiveSignCheck, downloadLimiter, handles.ArchiveDown) - g.GET("/ap/*path", archiveSignCheck, downloadLimiter, handles.ArchiveProxy) - g.GET("/ae/*path", archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract) - g.HEAD("/ad/*path", archiveSignCheck, handles.ArchiveDown) - g.HEAD("/ap/*path", archiveSignCheck, handles.ArchiveProxy) - g.HEAD("/ae/*path", archiveSignCheck, handles.ArchiveInternalExtract) + g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown) + g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveProxy) + g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract) + g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown) + g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveProxy) + g.HEAD("/ae/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveInternalExtract) + + g.GET("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown) + g.GET("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown) + g.HEAD("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingDown) + g.HEAD("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingDown) + g.GET("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract) + g.GET("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract) + g.HEAD("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract) + g.HEAD("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract) api := g.Group("/api") - auth := api.Group("", middlewares.Auth) + auth := api.Group("", middlewares.Auth(false)) webauthn := api.Group("/authn", middlewares.Authn) api.POST("/auth/login", handles.Login) @@ -93,7 +102,9 @@ func Init(e *gin.Engine) { public.Any("/archive_extensions", handles.ArchiveExtensions) _fs(auth.Group("/fs")) + fsAndShare(api.Group("/fs", middlewares.Auth(true))) _task(auth.Group("/task", middlewares.AuthNotGuest)) + _sharing(auth.Group("/share", middlewares.AuthNotGuest)) admin(auth.Group("/admin", middlewares.AuthAdmin)) if flags.Debug || flags.Dev { debug(g.Group("/debug")) @@ -169,10 +180,16 @@ func admin(g *gin.RouterGroup) { index.GET("/progress", middlewares.SearchIndex, handles.GetProgress) } +func fsAndShare(g *gin.RouterGroup) { + g.Any("/list", handles.FsListSplit) + g.Any("/get", handles.FsGetSplit) + a := g.Group("/archive") + a.Any("/meta", handles.FsArchiveMetaSplit) + a.Any("/list", handles.FsArchiveListSplit) +} + func _fs(g *gin.RouterGroup) { - g.Any("/list", handles.FsList) g.Any("/search", middlewares.SearchIndex, handles.Search) - g.Any("/get", handles.FsGet) g.Any("/other", handles.FsOther) g.Any("/dirs", handles.FsDirs) g.POST("/mkdir", handles.FsMkdir) @@ -192,16 +209,23 @@ func _fs(g *gin.RouterGroup) { // g.POST("/add_qbit", handles.AddQbittorrent) // g.POST("/add_transmission", handles.SetTransmission) g.POST("/add_offline_download", handles.AddOfflineDownload) - a := g.Group("/archive") - a.Any("/meta", handles.FsArchiveMeta) - a.Any("/list", handles.FsArchiveList) - a.POST("/decompress", handles.FsArchiveDecompress) + g.POST("/archive/decompress", handles.FsArchiveDecompress) } func _task(g *gin.RouterGroup) { handles.SetupTaskRoute(g) } +func _sharing(g *gin.RouterGroup) { + g.Any("/list", handles.ListSharings) + g.GET("/get", handles.GetSharing) + g.POST("/create", handles.CreateSharing) + g.POST("/update", handles.UpdateSharing) + g.POST("/delete", handles.DeleteSharing) + g.POST("/enable", handles.SetEnableSharing(false)) + g.POST("/disable", handles.SetEnableSharing(true)) +} + func Cors(r *gin.Engine) { config := cors.DefaultConfig() // config.AllowAllOrigins = true