diff --git a/Dockerfile.ci b/Dockerfile.ci
index a17aae9f..6075acc6 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -1,4 +1,4 @@
-FROM alpine:edge
+FROM alpine:3.20.7
ARG TARGETPLATFORM
ARG INSTALL_FFMPEG=false
@@ -31,4 +31,4 @@ RUN /entrypoint.sh version
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
VOLUME /opt/alist/data/
EXPOSE 5244 5245
-CMD [ "/entrypoint.sh" ]
\ No newline at end of file
+CMD [ "/entrypoint.sh" ]
diff --git a/README.md b/README.md
index 2bd7e812..5a93997f 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,10 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
+## API Documentation (via Apifox):
+
+
+
## Demo
diff --git a/README_cn.md b/README_cn.md
index 9052e79b..79ed864b 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -99,6 +99,10 @@
+## API 文档(通过 Apifox 提供)
+
+
+
## Demo
diff --git a/README_ja.md b/README_ja.md
index 4dcdfd20..9291b2ac 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -100,6 +100,10 @@
+## APIドキュメント(Apifox 提供)
+
+
+
## デモ
diff --git a/cmd/common.go b/cmd/common.go
index 8a73f9b0..d88a86eb 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
"os"
"path/filepath"
"strconv"
@@ -16,6 +17,12 @@ func Init() {
bootstrap.InitConfig()
bootstrap.Log()
bootstrap.InitDB()
+
+ if v3_46_0.IsLegacyRoleDetected() {
+ utils.Log.Warnf("Detected legacy role format, executing ConvertLegacyRoles patch early...")
+ v3_46_0.ConvertLegacyRoles()
+ }
+
data.InitData()
bootstrap.InitStreamLimit()
bootstrap.InitIndex()
diff --git a/internal/bootstrap/patch/v3_46_0/convert_role.go b/internal/bootstrap/patch/v3_46_0/convert_role.go
index 43799485..3aac95b6 100644
--- a/internal/bootstrap/patch/v3_46_0/convert_role.go
+++ b/internal/bootstrap/patch/v3_46_0/convert_role.go
@@ -1,7 +1,10 @@
package v3_46_0
import (
+ "database/sql"
+ "encoding/json"
"errors"
+ "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
@@ -83,47 +86,101 @@ func ConvertLegacyRoles() {
}
}
- users, _, err := op.GetUsers(1, -1)
+ rawDb := db.GetDb()
+ table := conf.Conf.Database.TablePrefix + "users"
+ rows, err := rawDb.Table(table).Select("id, username, role").Rows()
if err != nil {
utils.Log.Errorf("[convert roles] failed to get users: %v", err)
return
}
+ defer rows.Close()
- for i := range users {
- user := users[i]
- if user.Role == nil {
+ var updatedCount int
+ for rows.Next() {
+ var id uint
+ var username string
+ var rawRole []byte
+
+ if err := rows.Scan(&id, &username, &rawRole); err != nil {
+ utils.Log.Warnf("[convert roles] skip user scan err: %v", err)
continue
}
- changed := false
- var roles model.Roles
- for _, r := range user.Role {
+
+ utils.Log.Debugf("[convert roles] user: %s raw role: %s", username, string(rawRole))
+
+ if len(rawRole) == 0 {
+ continue
+ }
+
+ var oldRoles []int
+ wasSingleInt := false
+ if err := json.Unmarshal(rawRole, &oldRoles); err != nil {
+ var single int
+ if err := json.Unmarshal(rawRole, &single); err != nil {
+ utils.Log.Warnf("[convert roles] user %s has invalid role: %s", username, string(rawRole))
+ continue
+ }
+ oldRoles = []int{single}
+ wasSingleInt = true
+ }
+
+ var newRoles model.Roles
+ for _, r := range oldRoles {
switch r {
case model.ADMIN:
- roles = append(roles, int(adminRole.ID))
- if int(adminRole.ID) != r {
- changed = true
- }
+ newRoles = append(newRoles, int(adminRole.ID))
case model.GUEST:
- roles = append(roles, int(guestRole.ID))
- if int(guestRole.ID) != r {
- changed = true
- }
+ newRoles = append(newRoles, int(guestRole.ID))
case model.GENERAL:
- roles = append(roles, int(generalRole.ID))
- if int(generalRole.ID) != r {
- changed = true
- }
+ newRoles = append(newRoles, int(generalRole.ID))
default:
- roles = append(roles, r)
+ newRoles = append(newRoles, r)
}
}
- if changed {
- user.Role = roles
- if err := db.UpdateUser(&user); err != nil {
- utils.Log.Errorf("[convert roles] failed to update user %s: %v", user.Username, err)
+
+ if wasSingleInt {
+ err := rawDb.Table(table).Where("id = ?", id).Update("role", newRoles).Error
+ if err != nil {
+ utils.Log.Errorf("[convert roles] failed to update user %s: %v", username, err)
+ } else {
+ updatedCount++
+ utils.Log.Infof("[convert roles] updated user %s: %v → %v", username, oldRoles, newRoles)
}
}
}
- utils.Log.Infof("[convert roles] completed role conversion for %d users", len(users))
+ utils.Log.Infof("[convert roles] completed role conversion for %d users", updatedCount)
+}
+
+func IsLegacyRoleDetected() bool {
+ rawDb := db.GetDb()
+ table := conf.Conf.Database.TablePrefix + "users"
+ rows, err := rawDb.Table(table).Select("role").Rows()
+ if err != nil {
+ utils.Log.Errorf("[role check] failed to scan user roles: %v", err)
+ return false
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var raw sql.RawBytes
+ if err := rows.Scan(&raw); err != nil {
+ continue
+ }
+ if len(raw) == 0 {
+ continue
+ }
+
+ var roles []int
+ if err := json.Unmarshal(raw, &roles); err == nil {
+ continue
+ }
+
+ var single int
+ if err := json.Unmarshal(raw, &single); err == nil {
+ utils.Log.Infof("[role check] detected legacy int role: %d", single)
+ return true
+ }
+ }
+ return false
}
diff --git a/internal/op/role.go b/internal/op/role.go
index b7474566..b312f8c7 100644
--- a/internal/op/role.go
+++ b/internal/op/role.go
@@ -2,6 +2,7 @@ package op
import (
"fmt"
+ "github.com/pkg/errors"
"time"
"github.com/Xhofe/go-cache"
@@ -106,6 +107,20 @@ 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
+ modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath)
+ 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)
diff --git a/internal/op/storage.go b/internal/op/storage.go
index 2ec68aae..2833afa8 100644
--- a/internal/op/storage.go
+++ b/internal/op/storage.go
@@ -46,6 +46,11 @@ func GetStorageByMountPath(mountPath string) (driver.Driver, error) {
func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
+
+ if storage.MountPath == "/" {
+ return 0, errors.New("Mount path cannot be '/'")
+ }
+
var err error
// check driver first
driverName := storage.Driver
@@ -205,6 +210,9 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
}
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
+ if storage.MountPath == "/" {
+ return errors.New("Mount path cannot be '/'")
+ }
err = db.UpdateStorage(&storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in database")
diff --git a/internal/op/user.go b/internal/op/user.go
index b9662015..942daae6 100644
--- a/internal/op/user.go
+++ b/internal/op/user.go
@@ -78,7 +78,25 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
func CreateUser(u *model.User) error {
u.BasePath = utils.FixAndCleanPath(u.BasePath)
- return db.CreateUser(u)
+
+ err := db.CreateUser(u)
+ if err != nil {
+ return err
+ }
+
+ 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
+ }
+ }
+ _ = db.UpdateUser(u)
+ userCache.Del(u.Username)
+ }
+
+ return nil
}
func DeleteUserById(id uint) error {
@@ -106,6 +124,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
+ }
+ }
+ }
+ }
return db.UpdateUser(u)
}
diff --git a/server/handles/user.go b/server/handles/user.go
index 858c2a3b..d5eebba4 100644
--- a/server/handles/user.go
+++ b/server/handles/user.go
@@ -1,6 +1,7 @@
package handles
import (
+ "github.com/alist-org/alist/v3/pkg/utils"
"strconv"
"github.com/alist-org/alist/v3/internal/model"
@@ -60,10 +61,18 @@ func UpdateUser(c *gin.Context) {
common.ErrorResp(c, err, 500)
return
}
- //if !utils.SliceEqual(user.Role, req.Role) {
- // common.ErrorStrResp(c, "role can not be changed", 400)
- // return
- //}
+
+ if user.Username == "admin" {
+ if !utils.SliceEqual(user.Role, req.Role) {
+ 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 req.Password == "" {
req.PwdHash = user.PwdHash
req.Salt = user.Salt