mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-25 11:29:45 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcfb3369d1 | ||
|
|
aea3ba1499 | ||
|
|
6b2d81eede |
@@ -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), new(model.Role), new(model.Label), new(model.LabelFileBinDing), new(model.ObjFile))
|
||||
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.Role), new(model.Label), new(model.LabelFileBinding), new(model.ObjFile))
|
||||
if err != nil {
|
||||
log.Fatalf("failed migrate database: %s", err.Error())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetLabelIds Get all label_ids from database order by file_name
|
||||
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||
labelFileBinDingDB := db.Model(&model.LabelFileBinDing{})
|
||||
//fmt.Printf(">>> [GetLabelIds] userId: %d, fileName: %s\n", userId, fileName)
|
||||
labelFileBinDingDB := db.Model(&model.LabelFileBinding{})
|
||||
var labelIds []uint
|
||||
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@@ -18,7 +21,7 @@ func GetLabelIds(userId uint, fileName string) ([]uint, error) {
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||
var labelFileBinDing model.LabelFileBinDing
|
||||
var labelFileBinDing model.LabelFileBinding
|
||||
labelFileBinDing.UserId = userId
|
||||
labelFileBinDing.LabelId = labelId
|
||||
labelFileBinDing.FileName = fileName
|
||||
@@ -32,7 +35,7 @@ func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
|
||||
|
||||
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
|
||||
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
||||
var labelFileBinDing model.LabelFileBinDing
|
||||
var labelFileBinDing model.LabelFileBinding
|
||||
result := db.Where("label_id = ?", labelId).Where("user_id = ?", userId).First(&labelFileBinDing)
|
||||
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||||
return exists
|
||||
@@ -40,17 +43,150 @@ func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
|
||||
|
||||
// DelLabelFileBinDingByFileName used to del usually
|
||||
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
|
||||
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
}
|
||||
|
||||
// DelLabelFileBinDingById used to del usually
|
||||
func DelLabelFileBinDingById(labelId, userId uint, fileName string) error {
|
||||
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
|
||||
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
|
||||
}
|
||||
|
||||
func GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinDing, err error) {
|
||||
func GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinding, err error) {
|
||||
if err := db.Where("label_id in (?)", labelIds).Where("user_id = ?", userId).Find(&result).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetLabelBindingsByFileNamesPublic(fileNames []string) (map[string][]uint, error) {
|
||||
var binds []model.LabelFileBinding
|
||||
if err := db.Where("file_name IN ?", fileNames).Find(&binds).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
out := make(map[string][]uint, len(fileNames))
|
||||
seen := make(map[string]struct{}, len(binds))
|
||||
for _, b := range binds {
|
||||
key := fmt.Sprintf("%s-%d", b.FileName, b.LabelId)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out[b.FileName] = append(out[b.FileName], b.LabelId)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
bindMap, err := GetLabelBindingsByFileNamesPublic(fileNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idSet := make(map[uint]struct{})
|
||||
for _, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
idSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return make(map[string][]model.Label, 0), nil
|
||||
}
|
||||
allIDs := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
allIDs = append(allIDs, id)
|
||||
}
|
||||
labels, err := GetLabelByIds(allIDs) // 你已有的函数
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelByID := make(map[uint]model.Label, len(labels))
|
||||
for _, l := range labels {
|
||||
labelByID[l.ID] = l
|
||||
}
|
||||
|
||||
out := make(map[string][]model.Label, len(bindMap))
|
||||
for fname, ids := range bindMap {
|
||||
for _, id := range ids {
|
||||
if lab, ok := labelByID[id]; ok {
|
||||
out[fname] = append(out[fname], lab)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListLabelFileBinDing(userId uint, labelIDs []uint, fileName string, page, pageSize int) ([]model.LabelFileBinding, int64, error) {
|
||||
q := db.Model(&model.LabelFileBinding{}).Where("user_id = ?", userId)
|
||||
|
||||
if len(labelIDs) > 0 {
|
||||
q = q.Where("label_id IN ?", labelIDs)
|
||||
}
|
||||
if fileName != "" {
|
||||
q = q.Where("file_name LIKE ?", "%"+fileName+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var rows []model.LabelFileBinding
|
||||
if err := q.
|
||||
Order("id DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx := db.Begin()
|
||||
|
||||
if override {
|
||||
type key struct {
|
||||
uid uint
|
||||
name string
|
||||
}
|
||||
toDel := make(map[key]struct{}, len(bindings))
|
||||
for i := range bindings {
|
||||
k := key{uid: bindings[i].UserId, name: bindings[i].FileName}
|
||||
toDel[k] = struct{}{}
|
||||
}
|
||||
for k := range toDel {
|
||||
if err := tx.Where("user_id = ? AND file_name = ?", k.uid, k.name).
|
||||
Delete(&model.LabelFileBinding{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range bindings {
|
||||
b := bindings[i]
|
||||
if !keepIDs {
|
||||
b.ID = 0
|
||||
}
|
||||
if b.CreateTime.IsZero() {
|
||||
b.CreateTime = time.Now()
|
||||
}
|
||||
if override {
|
||||
if err := tx.Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&b).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(tx.Commit().Error)
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package errs
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrChangeDefaultRole = errors.New("cannot modify admin or guest role")
|
||||
ErrChangeDefaultRole = errors.New("cannot modify admin role")
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
type LabelFileBinDing struct {
|
||||
type LabelFileBinding struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"` // unique key
|
||||
UserId uint `json:"user_id"` // use to user_id
|
||||
LabelId uint `json:"label_id"` // use to label_id
|
||||
|
||||
@@ -145,13 +145,28 @@ func (u *User) CheckPathLimit() bool {
|
||||
}
|
||||
|
||||
func (u *User) JoinPath(reqPath string) (string, error) {
|
||||
if reqPath == "/" {
|
||||
return utils.FixAndCleanPath(u.BasePath), nil
|
||||
}
|
||||
path, err := utils.JoinBasePath(u.BasePath, reqPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.CheckPathLimit() && !utils.IsSubPath(u.BasePath, path) {
|
||||
return "", errs.PermissionDenied
|
||||
|
||||
if path != "/" && u.CheckPathLimit() {
|
||||
basePaths := GetAllBasePathsFromRoles(u)
|
||||
match := false
|
||||
for _, base := range basePaths {
|
||||
if utils.IsSubPath(base, path) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return "", errs.PermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
@@ -193,3 +208,33 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return "https://alistgo.com/logo.svg"
|
||||
}
|
||||
|
||||
// FetchRole is used to load role details by id. It should be set by the op package
|
||||
// to avoid an import cycle between model and op.
|
||||
var FetchRole func(uint) (*Role, error)
|
||||
|
||||
// GetAllBasePathsFromRoles returns all permission paths from user's roles
|
||||
func GetAllBasePathsFromRoles(u *User) []string {
|
||||
basePaths := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, rid := range u.Role {
|
||||
if FetchRole == nil {
|
||||
continue
|
||||
}
|
||||
role, err := FetchRole(uint(rid))
|
||||
if err != nil || role == nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if entry.Path == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[entry.Path]; !ok {
|
||||
basePaths = append(basePaths, entry.Path)
|
||||
seen[entry.Path] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return basePaths
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type CreateLabelFileBinDingReq struct {
|
||||
Type int `json:"type"`
|
||||
HashInfoStr string `json:"hashinfo"`
|
||||
LabelIds string `json:"label_ids"`
|
||||
LabelIDs []uint64 `json:"labelIdList"`
|
||||
}
|
||||
|
||||
type ObjLabelResp struct {
|
||||
@@ -54,23 +55,29 @@ func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
|
||||
return db.GetLabelsByFileNamesPublic(fileNames)
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
|
||||
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
|
||||
}
|
||||
if req.LabelIds == "" {
|
||||
|
||||
ids, err := collectLabelIDs(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
labelMap := strings.Split(req.LabelIds, ",")
|
||||
for _, value := range labelMap {
|
||||
labelId, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid label ID '%s': %v", value, err)
|
||||
}
|
||||
if err = db.CreateLabelFileBinDing(req.Name, uint(labelId), userId); err != nil {
|
||||
|
||||
for _, id := range ids {
|
||||
if err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {
|
||||
return errors.WithMessage(err, "failed labels in database")
|
||||
}
|
||||
}
|
||||
|
||||
if !db.GetFileByNameExists(req.Name) {
|
||||
objFile := model.ObjFile{
|
||||
Id: req.Id,
|
||||
@@ -86,8 +93,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
Type: req.Type,
|
||||
HashInfoStr: req.HashInfoStr,
|
||||
}
|
||||
err := db.CreateObjFile(objFile)
|
||||
if err != nil {
|
||||
if err := db.CreateObjFile(objFile); err != nil {
|
||||
return errors.WithMessage(err, "failed file in database")
|
||||
}
|
||||
}
|
||||
@@ -97,7 +103,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
|
||||
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
|
||||
labelMap := strings.Split(labelId, ",")
|
||||
var labelIds []uint
|
||||
var labelsFile []model.LabelFileBinDing
|
||||
var labelsFile []model.LabelFileBinding
|
||||
var labels []model.Label
|
||||
var labelsFileMap = make(map[string][]model.Label)
|
||||
var labelsMap = make(map[uint]model.Label)
|
||||
@@ -157,3 +163,33 @@ func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
|
||||
}
|
||||
return uintSlice, nil
|
||||
}
|
||||
|
||||
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
|
||||
return db.RestoreLabelFileBindings(bindings, keepIDs, override)
|
||||
}
|
||||
|
||||
func collectLabelIDs(req CreateLabelFileBinDingReq) ([]uint64, error) {
|
||||
if len(req.LabelIDs) > 0 {
|
||||
return req.LabelIDs, nil
|
||||
}
|
||||
s := strings.TrimSpace(req.LabelIds)
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
replacer := strings.NewReplacer(",", ",", "、", ",", ";", ",", ";", ",")
|
||||
s = replacer.Replace(s)
|
||||
parts := strings.Split(s, ",")
|
||||
ids := make([]uint64, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseUint(p, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid label ID '%s': %v", p, err)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
@@ -16,6 +15,10 @@ import (
|
||||
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
|
||||
var roleG singleflight.Group[*model.Role]
|
||||
|
||||
func init() {
|
||||
model.FetchRole = GetRole
|
||||
}
|
||||
|
||||
func GetRole(id uint) (*model.Role, error) {
|
||||
key := fmt.Sprint(id)
|
||||
if r, ok := roleCache.Get(key); ok {
|
||||
@@ -106,26 +109,26 @@ func UpdateRole(r *model.Role) error {
|
||||
for i := range r.PermissionScopes {
|
||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||
}
|
||||
if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
|
||||
oldPath := old.PermissionScopes[0].Path
|
||||
newPath := r.PermissionScopes[0].Path
|
||||
|
||||
users, err := db.GetUsersByRole(int(r.ID))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get users by role")
|
||||
}
|
||||
|
||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
}
|
||||
|
||||
for _, name := range modifiedUsernames {
|
||||
userCache.Del(name)
|
||||
}
|
||||
}
|
||||
//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
// old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
//
|
||||
// oldPath := old.PermissionScopes[0].Path
|
||||
// newPath := r.PermissionScopes[0].Path
|
||||
//
|
||||
// users, err := db.GetUsersByRole(int(r.ID))
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to get users by role")
|
||||
// }
|
||||
//
|
||||
// modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
|
||||
// if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
// }
|
||||
//
|
||||
// for _, name := range modifiedUsernames {
|
||||
// userCache.Del(name)
|
||||
// }
|
||||
//}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
return db.UpdateRole(r)
|
||||
|
||||
@@ -232,12 +232,20 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
}
|
||||
|
||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update user base path")
|
||||
}
|
||||
for _, name := range modifiedUsernames {
|
||||
userCache.Del(name)
|
||||
//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
|
||||
//if err != nil {
|
||||
// return errors.WithMessage(err, "failed to update user base path")
|
||||
//}
|
||||
for _, id := range modifiedRoleIDs {
|
||||
roleCache.Del(fmt.Sprint(id))
|
||||
|
||||
users, err := db.GetUsersByRole(int(id))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get users by role")
|
||||
}
|
||||
for _, user := range users {
|
||||
userCache.Del(user.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -50,6 +50,10 @@ func GetUserByRole(role int) (*model.User, error) {
|
||||
return db.GetUserByRole(role)
|
||||
}
|
||||
|
||||
func GetUsersByRole(role int) ([]model.User, error) {
|
||||
return db.GetUsersByRole(role)
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
if username == "" {
|
||||
return nil, errs.EmptyUsername
|
||||
@@ -124,17 +128,17 @@ func UpdateUser(u *model.User) error {
|
||||
}
|
||||
userCache.Del(old.Username)
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
if len(u.Role) > 0 {
|
||||
roles, err := GetRolesByUserID(u.ID)
|
||||
if err == nil {
|
||||
for _, role := range roles {
|
||||
if len(role.PermissionScopes) > 0 {
|
||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//if len(u.Role) > 0 {
|
||||
// roles, err := GetRolesByUserID(u.ID)
|
||||
// if err == nil {
|
||||
// for _, role := range roles {
|
||||
// if len(role.PermissionScopes) > 0 {
|
||||
// u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
return db.UpdateUser(u)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,13 @@ func JoinBasePath(basePath, reqPath string) (string, error) {
|
||||
strings.Contains(reqPath, "/../") {
|
||||
return "", errs.RelativePath
|
||||
}
|
||||
|
||||
reqPath = FixAndCleanPath(reqPath)
|
||||
|
||||
if strings.HasPrefix(reqPath, "/") {
|
||||
return reqPath, nil
|
||||
}
|
||||
|
||||
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,17 +43,23 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
} else {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
perm |= entry.Permission
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return perm
|
||||
}
|
||||
|
||||
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
|
||||
if !canReadPathByRole(u, reqPath) {
|
||||
if !CanReadPathByRole(u, reqPath) {
|
||||
return false
|
||||
}
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
@@ -78,7 +84,30 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin
|
||||
return meta.Password == password
|
||||
}
|
||||
|
||||
func canReadPathByRole(u *model.User, reqPath string) bool {
|
||||
func CanReadPathByRole(u *model.User, reqPath string) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
|
||||
return len(u.Role) > 0
|
||||
}
|
||||
for _, rid := range u.Role {
|
||||
role, err := op.GetRole(uint(rid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasChildPermission checks whether any child path under reqPath grants the
|
||||
// specified permission bit.
|
||||
func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
@@ -88,7 +117,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
|
||||
continue
|
||||
}
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.IsSubPath(entry.Path, reqPath) {
|
||||
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -102,7 +131,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
|
||||
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
|
||||
perm := MergeRolePermissions(u, reqPath)
|
||||
if HasPermission(perm, PermPathLimit) {
|
||||
return canReadPathByRole(u, reqPath)
|
||||
return CanReadPathByRole(u, reqPath)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -107,14 +107,21 @@ func FsList(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
total, objs := pagination(objs, &req.PageReq)
|
||||
filtered := make([]model.Obj, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
childPath := stdpath.Join(reqPath, obj.GetName())
|
||||
if common.CanReadPathByRole(user, childPath) {
|
||||
filtered = append(filtered, obj)
|
||||
}
|
||||
}
|
||||
total, objs := pagination(filtered, &req.PageReq)
|
||||
provider := "unknown"
|
||||
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
||||
if err == nil {
|
||||
provider = storage.GetStorage().Driver
|
||||
}
|
||||
common.SuccessResp(c, FsListResp{
|
||||
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath), user.ID),
|
||||
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)),
|
||||
Total: int64(total),
|
||||
Readme: getReadme(meta, reqPath),
|
||||
Header: getHeader(meta, reqPath),
|
||||
@@ -161,7 +168,14 @@ func FsDirs(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
dirs := filterDirs(objs)
|
||||
visible := make([]model.Obj, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
childPath := stdpath.Join(reqPath, obj.GetName())
|
||||
if common.CanReadPathByRole(user, childPath) {
|
||||
visible = append(visible, obj)
|
||||
}
|
||||
}
|
||||
dirs := filterDirs(visible)
|
||||
common.SuccessResp(c, dirs)
|
||||
}
|
||||
|
||||
@@ -224,12 +238,22 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
|
||||
return total, objs[start:end]
|
||||
}
|
||||
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool, userId uint) []ObjLabelResp {
|
||||
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjLabelResp {
|
||||
var resp []ObjLabelResp
|
||||
|
||||
names := make([]string, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
if !obj.IsDir() {
|
||||
names = append(names, obj.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
labelsByName, _ := op.GetLabelsByFileNamesPublic(names)
|
||||
|
||||
for _, obj := range objs {
|
||||
var labels []model.Label
|
||||
if obj.IsDir() == false {
|
||||
labels, _ = op.GetLabelByFileName(userId, obj.GetName())
|
||||
if !obj.IsDir() {
|
||||
labels = labelsByName[obj.GetName()]
|
||||
}
|
||||
thumb, _ := model.GetThumb(obj)
|
||||
resp = append(resp, ObjLabelResp{
|
||||
@@ -369,7 +393,7 @@ func FsGet(c *gin.Context) {
|
||||
Readme: getReadme(meta, reqPath),
|
||||
Header: getHeader(meta, reqPath),
|
||||
Provider: provider,
|
||||
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath), user.ID),
|
||||
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DelLabelFileBinDingReq struct {
|
||||
@@ -16,18 +18,36 @@ type DelLabelFileBinDingReq struct {
|
||||
LabelId string `json:"label_id"`
|
||||
}
|
||||
|
||||
type pageResp[T any] struct {
|
||||
Content []T `json:"content"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type restoreLabelBindingsReq struct {
|
||||
KeepIDs bool `json:"keep_ids"`
|
||||
Override bool `json:"override"`
|
||||
Bindings []model.LabelFileBinding `json:"bindings"`
|
||||
}
|
||||
|
||||
func GetLabelByFileName(c *gin.Context) {
|
||||
fileName := c.Query("file_name")
|
||||
if fileName == "" {
|
||||
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
|
||||
return
|
||||
}
|
||||
decodedFileName, err := url.QueryUnescape(fileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, errors.New("invalid file_name"), 400)
|
||||
return
|
||||
}
|
||||
fmt.Println(">>> 原始 fileName:", fileName)
|
||||
fmt.Println(">>> 解码后 fileName:", decodedFileName)
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
labels, err := op.GetLabelByFileName(userObj.ID, fileName)
|
||||
labels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
@@ -101,3 +121,130 @@ func GetFileByLabel(c *gin.Context) {
|
||||
}
|
||||
common.SuccessResp(c, fileList)
|
||||
}
|
||||
|
||||
func ListLabelFileBinding(c *gin.Context) {
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
sizeStr := c.DefaultQuery("page_size", "50")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, err := strconv.Atoi(sizeStr)
|
||||
if err != nil || pageSize <= 0 || pageSize > 200 {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
fileName := c.Query("file_name")
|
||||
labelIDStr := c.Query("label_id")
|
||||
var labelIDs []uint
|
||||
if labelIDStr != "" {
|
||||
parts := strings.Split(labelIDStr, ",")
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
id64, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, fmt.Errorf("invalid label_id '%s': %v", p, err), 400)
|
||||
return
|
||||
}
|
||||
labelIDs = append(labelIDs, uint(id64))
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := db.ListLabelFileBinDing(userObj.ID, labelIDs, fileName, page, pageSize)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, pageResp[model.LabelFileBinding]{
|
||||
Content: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
func RestoreLabelFileBinding(c *gin.Context) {
|
||||
var req restoreLabelBindingsReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if len(req.Bindings) == 0 {
|
||||
common.ErrorStrResp(c, "empty bindings", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if u, ok := c.Value("user").(*model.User); ok {
|
||||
for i := range req.Bindings {
|
||||
if req.Bindings[i].UserId == 0 {
|
||||
req.Bindings[i].UserId = u.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range req.Bindings {
|
||||
b := req.Bindings[i]
|
||||
if b.UserId == 0 || b.LabelId == 0 || strings.TrimSpace(b.FileName) == "" {
|
||||
common.ErrorStrResp(c, "invalid binding: user_id/label_id/file_name required", 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := op.RestoreLabelFileBindings(req.Bindings, req.KeepIDs, req.Override); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, gin.H{
|
||||
"msg": fmt.Sprintf("restored %d rows", len(req.Bindings)),
|
||||
})
|
||||
}
|
||||
|
||||
func CreateLabelFileBinDingBatch(c *gin.Context) {
|
||||
var req struct {
|
||||
Items []op.CreateLabelFileBinDingReq `json:"items" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
|
||||
userObj, ok := c.Value("user").(*model.User)
|
||||
if !ok {
|
||||
common.ErrorStrResp(c, "user invalid", 401)
|
||||
return
|
||||
}
|
||||
|
||||
type perResult struct {
|
||||
Name string `json:"name"`
|
||||
Ok bool `json:"ok"`
|
||||
ErrMsg string `json:"errMsg,omitempty"`
|
||||
}
|
||||
results := make([]perResult, 0, len(req.Items))
|
||||
succeed := 0
|
||||
|
||||
for _, item := range req.Items {
|
||||
if item.IsDir {
|
||||
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: "Unable to bind folder"})
|
||||
continue
|
||||
}
|
||||
if err := op.CreateLabelFileBinDing(item, userObj.ID); err != nil {
|
||||
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: err.Error()})
|
||||
continue
|
||||
}
|
||||
succeed++
|
||||
results = append(results, perResult{Name: item.Name, Ok: true})
|
||||
}
|
||||
|
||||
common.SuccessResp(c, gin.H{
|
||||
"total": len(req.Items),
|
||||
"succeed": succeed,
|
||||
"failed": len(req.Items) - succeed,
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ func UpdateUser(c *gin.Context) {
|
||||
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
||||
return
|
||||
}
|
||||
if user.Username != req.Username {
|
||||
common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
return
|
||||
}
|
||||
//if user.Username != req.Username {
|
||||
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
|
||||
@@ -92,6 +92,8 @@ func Init(e *gin.Engine) {
|
||||
|
||||
_fs(auth.Group("/fs"))
|
||||
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
||||
_label(auth.Group("/label"))
|
||||
_labelFileBinding(auth.Group("/label_file_binding"))
|
||||
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
||||
if flags.Debug || flags.Dev {
|
||||
debug(g.Group("/debug"))
|
||||
@@ -170,17 +172,17 @@ func admin(g *gin.RouterGroup) {
|
||||
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
||||
|
||||
label := g.Group("/label")
|
||||
label.GET("/list", handles.ListLabel)
|
||||
label.GET("/get", handles.GetLabel)
|
||||
label.POST("/create", handles.CreateLabel)
|
||||
label.POST("/update", handles.UpdateLabel)
|
||||
label.POST("/delete", handles.DeleteLabel)
|
||||
|
||||
labelFileBinding := g.Group("/label_file_binding")
|
||||
labelFileBinding.GET("/get", handles.GetLabelByFileName)
|
||||
labelFileBinding.GET("/get_file_by_label", handles.GetFileByLabel)
|
||||
labelFileBinding.GET("/list", handles.ListLabelFileBinding)
|
||||
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
|
||||
labelFileBinding.POST("/create_batch", handles.CreateLabelFileBinDingBatch)
|
||||
labelFileBinding.POST("/delete", handles.DelLabelByFileName)
|
||||
labelFileBinding.POST("/restore", handles.RestoreLabelFileBinding)
|
||||
|
||||
}
|
||||
|
||||
func _fs(g *gin.RouterGroup) {
|
||||
@@ -216,6 +218,16 @@ func _task(g *gin.RouterGroup) {
|
||||
handles.SetupTaskRoute(g)
|
||||
}
|
||||
|
||||
func _label(g *gin.RouterGroup) {
|
||||
g.GET("/list", handles.ListLabel)
|
||||
g.GET("/get", handles.GetLabel)
|
||||
}
|
||||
|
||||
func _labelFileBinding(g *gin.RouterGroup) {
|
||||
g.GET("/get", handles.GetLabelByFileName)
|
||||
g.GET("/get_file_by_label", handles.GetFileByLabel)
|
||||
}
|
||||
|
||||
func Cors(r *gin.Engine) {
|
||||
config := cors.DefaultConfig()
|
||||
// config.AllowAllOrigins = true
|
||||
|
||||
@@ -95,6 +95,9 @@ func WebDAVAuth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
|
||||
user.RolesDetail = roles
|
||||
}
|
||||
reqPath := c.Param("path")
|
||||
if reqPath == "" {
|
||||
reqPath = "/"
|
||||
@@ -107,7 +110,8 @@ func WebDAVAuth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
perm := common.MergeRolePermissions(user, reqPath)
|
||||
if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) {
|
||||
webdavRead := common.HasPermission(perm, common.PermWebdavRead)
|
||||
if user.Disabled || (!webdavRead && (c.Request.Method != "PROPFIND" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) {
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.Set("user", guest)
|
||||
c.Next()
|
||||
|
||||
@@ -94,6 +94,7 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
||||
depth = 0
|
||||
}
|
||||
meta, _ := op.GetNearestMeta(name)
|
||||
user := ctx.Value("user").(*model.User)
|
||||
// Read directory names.
|
||||
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
|
||||
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
|
||||
@@ -108,6 +109,9 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
|
||||
|
||||
for _, fileInfo := range objs {
|
||||
filename := path.Join(name, fileInfo.GetName())
|
||||
if !common.CanReadPathByRole(user, filename) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||
return err
|
||||
|
||||
@@ -648,6 +648,98 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
||||
|
||||
mw := multistatusWriter{w: w}
|
||||
|
||||
if utils.PathEqual(reqPath, user.BasePath) {
|
||||
hasRootPerm := false
|
||||
for _, role := range user.RolesDetail {
|
||||
for _, entry := range role.PermissionScopes {
|
||||
if utils.PathEqual(entry.Path, user.BasePath) {
|
||||
hasRootPerm = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasRootPerm {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRootPerm {
|
||||
basePaths := model.GetAllBasePathsFromRoles(user)
|
||||
type infoItem struct {
|
||||
path string
|
||||
info model.Obj
|
||||
}
|
||||
infos := []infoItem{{reqPath, fi}}
|
||||
seen := make(map[string]struct{})
|
||||
for _, p := range basePaths {
|
||||
if !utils.IsSubPath(user.BasePath, p) {
|
||||
continue
|
||||
}
|
||||
rel := strings.TrimPrefix(
|
||||
strings.TrimPrefix(
|
||||
utils.FixAndCleanPath(p),
|
||||
utils.FixAndCleanPath(user.BasePath),
|
||||
),
|
||||
"/",
|
||||
)
|
||||
dir := strings.Split(rel, "/")[0]
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[dir]; ok {
|
||||
continue
|
||||
}
|
||||
seen[dir] = struct{}{}
|
||||
sp := utils.FixAndCleanPath(path.Join(user.BasePath, dir))
|
||||
info, err := fs.Get(ctx, sp, &fs.GetArgs{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, infoItem{sp, info})
|
||||
}
|
||||
for _, item := range infos {
|
||||
var pstats []Propstat
|
||||
if pf.Propname != nil {
|
||||
pnames, err := propnames(ctx, h.LockSystem, item.info)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
pstat := Propstat{Status: http.StatusOK}
|
||||
for _, xmlname := range pnames {
|
||||
pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
|
||||
}
|
||||
pstats = append(pstats, pstat)
|
||||
} else if pf.Allprop != nil {
|
||||
pstats, err = allprop(ctx, h.LockSystem, item.info, pf.Prop)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} else {
|
||||
pstats, err = props(ctx, h.LockSystem, item.info, pf.Prop)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
rel := strings.TrimPrefix(
|
||||
strings.TrimPrefix(
|
||||
utils.FixAndCleanPath(item.path),
|
||||
utils.FixAndCleanPath(user.BasePath),
|
||||
),
|
||||
"/",
|
||||
)
|
||||
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
|
||||
if href != "/" && item.info.IsDir() {
|
||||
href += "/"
|
||||
}
|
||||
if err := mw.write(makePropstatResponse(href, pstats)); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
if err := mw.close(); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
walkFn := func(reqPath string, info model.Obj, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -671,7 +763,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
|
||||
rel := strings.TrimPrefix(
|
||||
strings.TrimPrefix(
|
||||
utils.FixAndCleanPath(reqPath),
|
||||
utils.FixAndCleanPath(user.BasePath),
|
||||
),
|
||||
"/",
|
||||
)
|
||||
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
|
||||
if href != "/" && info.IsDir() {
|
||||
href += "/"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user