Compare commits

..

6 Commits

Author SHA1 Message Date
千石
a9fcd51bc4 fix: ensure DefaultRole stores role ID while exposing role name in APIs (#9279)
* fix(setting): ensure DefaultRole stores role ID while exposing role name in APIs

- Simplified initial settings to use `model.GUEST` as the default role ID instead of querying roles at startup.
- Updated `GetSetting`, `ListSettings` handlers to:
  - Convert stored role ID into the corresponding role name when returning data.
  - Preserve dynamic role options for selection.
- Removed unused `strings` import and role preloading logic from `InitialSettings`.
- This change avoids DB dependency during initialization while keeping consistent role display for frontend clients.

* fix(setting): ensure DefaultRole stores role ID while exposing role
name in APIs (fix/settings-get-role)

- Simplify initial settings to use `model.GUEST` as the default role ID
  instead of querying roles at startup.
- Update `GetSetting`, `ListSettings` handlers to:
  - Convert stored role ID into the corresponding role name when
    returning data.
  - Preserve dynamic role options for selection.
- Remove unused `strings` import and role preloading logic from
  `InitialSettings`.
- Avoid DB dependency during initialization while keeping consistent
  role display for frontend clients.
2025-08-19 15:01:32 +08:00
千石
74e384175b fix(lanzou): correct comment parsing logic in lanzou driver (#9278)
- Adjusted logic to skip incrementing index when exiting comments.
- Added checks to continue loop if inside a single-line or block comment.
- Prevents erroneous parsing and retains intended comment exclusion.
2025-08-19 00:53:52 +08:00
千石
eca500861a feat: add user registration endpoint and role-based default settings (#9277)
* feat(setting): add role-based default and registration settings (closed #feat/register-and-statistics)

- Added `AllowRegister` and `DefaultRole` settings to site configuration.
- Integrated dynamic role options for `DefaultRole` using `op.GetRoles`.
- Updated `setting.go` handlers to manage `DefaultRole` options dynamically.
- Modified `const.go` to include new site settings constants.
- Updated dependencies in `go.mod` and `go.sum` to support new functionality.

* feat(register-and-statistics): add user registration endpoint

- Added `POST /auth/register` endpoint to support user registration.
- Implemented registration logic in `auth.go` with dynamic role assignment.
- Integrated settings `AllowRegister` and `DefaultRole` for registration flow.
- Updated imports to include new modules: `conf`, `setting`.
- Adjusted user creation logic to use `DefaultRole` setting dynamically.

* feat(register-and-statistics): add user registration endpoint (#register-and-statistics)

- Added `POST /auth/register` endpoint to support user registration.
- Implemented registration logic in `auth.go` with dynamic role assignment.
- Integrated `AllowRegister` and `DefaultRole` settings for registration flow.
- Updated imports to include new modules: `conf`, `setting`.
- Adjusted user creation logic to use `DefaultRole` dynamically.

* feat(register-and-statistics): enhance role management logic (#register-and-statistics)

- Refactored CreateRole and UpdateRole functions to handle default role.
- Added dynamic role assignment logic in 'role.go' using conf settings.
- Improved request handling in 'handles/role.go' with structured data.
- Implemented default role logic in 'db/role.go' to update non-default roles.
- Modified 'model/role.go' to include a 'Default' field for role management.

* feat(register-and-statistics): enhance role management logic

- Refactor CreateRole and UpdateRole to handle default roles.
- Add dynamic role assignment using conf settings in 'role.go'.
- Improve request handling with structured data in 'handles/role.go'.
- Implement default role logic in 'db/role.go' for non-default roles.
- Modify 'model/role.go' to include 'Default' field for role management.

* feat(register-and-statistics): improve role handling logic

- Switch from role names to role IDs for better consistency.
- Update logic to prioritize "guest" for default role ID.
- Adjust `DefaultRole` setting to use role IDs.
- Refactor `getRoleOptions` to return role IDs as a comma-separated string.

* feat(register-and-statistics): improve role handling logic
2025-08-18 16:38:21 +08:00
千石
97d4f79b96 fix: resolve webdav decode issue (#9268)
* fix: resolve webdav handshake error in permission checks

- Updated role permission logic to handle bidirectional subpaths,
  fixing handshake termination by remote host due to path mismatch.
- Refactored function naming for consistency and clarity.
- Enhanced filtering of objects based on user permissions.
- Modified `makePropstatResponse` to preserve encoded href paths.
- Added test for `makePropstatResponse` to ensure href encoding.

* Delete server/webdav/makepropstatresponse_test.go

* ci(workflow): set GOPROXY for Go builds on GitHub Actions

- Use `GOPROXY=https://proxy.golang.org,direct` to speed up module downloads
- Mitigates network flakiness (e.g., checksum DB timeouts/rate limits)
- `,direct` provides fallback for private/unproxyable modules
- No build logic changes; only affects dependency resolution across all matrix targets

---------

Co-authored-by: AlistGo <opsgit88@gmail.com>
2025-08-16 20:55:17 +08:00
千石
fcfb3369d1 fix: webdav error location (#9266)
* feat: improve WebDAV permission handling and user role fetching

- Added logic to handle root permissions in WebDAV requests.
- Improved the user role fetching mechanism.
- Enhanced path checks and permission scopes in role_perm.go.
- Set FetchRole function to avoid import cycles between modules.

* fix(webdav): resolve connection reset issue by encoding paths

- Adjust path encoding in webdav.go to prevent connection reset.
- Utilize utils.EncodePath for correct path formatting.
- Ensure proper handling of directory paths with trailing slash.

* fix(webdav): resolve connection reset issue by encoding paths

- Adjust path encoding in webdav.go to prevent connection reset.
- Utilize utils.FixAndCleanPath for correct path formatting.
- Ensure proper handling of directory paths with trailing slash.

* fix: resolve webdav handshake error in permission checks

- Updated role permission logic to handle bidirectional subpaths.
- This adjustment fixes the issue where remote host terminates the
  handshake due to improper path matching.

* fix: resolve webdav handshake error in permission checks (fix/fix-webdav-error)

- Updated role permission logic to handle bidirectional subpaths,
  fixing handshake termination by remote host due to path mismatch.
- Refactored function naming for consistency and clarity.
- Enhanced filtering of objects based on user permissions.

* fix: resolve webdav handshake error in permission checks

- Updated role permission logic to handle bidirectional subpaths,
  fixing handshake termination by remote host due to path mismatch.
- Refactored function naming for consistency and clarity.
- Enhanced filtering of objects based on user permissions.
2025-08-15 23:10:55 +08:00
千石
aea3ba1499 feat: add tag backup and fix bugs (#9265)
* feat(label): enhance label file binding and router setup (feat/add-tag-backup)

- Add `GetLabelsByFileNamesPublic` to retrieve labels using file names.
- Refactor router setup for label and file binding routes.
- Improve `toObjsResp` for efficient label retrieval by file names.
- Comment out unnecessary user ID parameter in `toObjsResp`.

* feat(label): enhance label file binding and router setup

- Add `GetLabelsByFileNamesPublic` for label retrieval by file names.
- Refactor router setup for label and file binding routes.
- Improve `toObjsResp` for efficient label retrieval by file names.
- Comment out unnecessary user ID parameter in `toObjsResp`.

* refactor(db): comment out debug print in GetLabelIds (#feat/add-tag-backup)

- Comment out debug print statement in GetLabelIds to clean up logs.
- Enhance code readability by removing unnecessary debug output.

* feat(label-file-binding): add batch creation and improve label ID handling

- Introduced `CreateLabelFileBinDingBatch` API for batch label binding.
- Added `collectLabelIDs` helper function to handle label ID parsing.
- Enhanced label ID handling to support varied delimiters and input formats.
- Refactored `CreateLabelFileBinDing` logic for improved code readability.
- Updated router to include `POST /label_file_binding/create_batch`.
2025-08-15 23:09:00 +08:00
27 changed files with 783 additions and 71 deletions

View File

@@ -25,6 +25,8 @@ jobs:
- android-arm64
name: Build
runs-on: ${{ matrix.platform }}
env:
GOPROXY: https://proxy.golang.org,direct
steps:
- name: Checkout

View File

@@ -94,6 +94,7 @@ func RemoveJSComment(data string) string {
}
if inComment && v == '*' && i+1 < len(data) && data[i+1] == '/' {
inComment = false
i++
continue
}
if v == '/' && i+1 < len(data) {
@@ -108,6 +109,9 @@ func RemoveJSComment(data string) string {
continue
}
}
if inComment || inSingleLineComment {
continue
}
result.WriteByte(v)
}

9
go.mod
View File

@@ -3,6 +3,8 @@ module github.com/alist-org/alist/v3
go 1.23.4
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
github.com/KirCute/sftpd-alist v0.0.12
github.com/ProtonMail/go-crypto v1.0.0
@@ -79,11 +81,7 @@ require (
gorm.io/gorm v1.25.11
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
)
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
require (
github.com/STARRY-S/zip v0.2.1 // indirect
@@ -109,7 +107,6 @@ require (
github.com/ipfs/boxo v0.12.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78

13
go.sum
View File

@@ -21,10 +21,16 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -172,7 +178,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -398,6 +403,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -492,6 +499,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -739,8 +748,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

View File

@@ -91,6 +91,7 @@ func InitialSettings() []model.SettingItem {
} else {
token = random.Token()
}
defaultRoleID := strconv.Itoa(model.GUEST)
initialSettingItems = []model.SettingItem{
// site settings
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
@@ -103,6 +104,8 @@ func InitialSettings() []model.SettingItem {
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
{Key: conf.AllowRegister, Value: "false", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.DefaultRole, Value: defaultRoleID, Type: conf.TypeSelect, Group: model.SITE},
// newui settings
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
// style settings

View File

@@ -10,13 +10,15 @@ const (
const (
// site
VERSION = "version"
SiteTitle = "site_title"
Announcement = "announcement"
AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
UseNewui = "use_newui"
VERSION = "version"
SiteTitle = "site_title"
Announcement = "announcement"
AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
AllowRegister = "allow_register"
DefaultRole = "default_role"
UseNewui = "use_newui"
Logo = "logo"
Favicon = "favicon"

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -35,11 +35,27 @@ func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err err
}
func CreateRole(r *model.Role) error {
return errors.WithStack(db.Create(r).Error)
if err := db.Create(r).Error; err != nil {
return errors.WithStack(err)
}
if r.Default {
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
return errors.WithStack(err)
}
}
return nil
}
func UpdateRole(r *model.Role) error {
return errors.WithStack(db.Save(r).Error)
if err := db.Save(r).Error; err != nil {
return errors.WithStack(err)
}
if r.Default {
if err := db.Model(&model.Role{}).Where("id <> ?", r.ID).Update("default", false).Error; err != nil {
return errors.WithStack(err)
}
}
return nil
}
func DeleteRole(id uint) error {

View File

@@ -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")
)

View File

@@ -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

View File

@@ -17,6 +17,7 @@ type Role struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique" binding:"required"`
Description string `json:"description"`
Default bool `json:"default" gorm:"default:false"`
// PermissionScopes stores structured permission list and is ignored by gorm.
PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
// RawPermission is the JSON representation of PermissionScopes stored in DB.

View File

@@ -145,12 +145,15 @@ 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() {
if path != "/" && u.CheckPathLimit() {
basePaths := GetAllBasePathsFromRoles(u)
match := false
for _, base := range basePaths {
@@ -206,12 +209,23 @@ 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 _, role := range u.RolesDetail {
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

View File

@@ -2,6 +2,7 @@ package op
import (
"regexp"
"strconv"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
@@ -82,6 +83,18 @@ var settingItemHooks = map[string]SettingItemHook{
conf.SlicesMap[conf.IgnoreDirectLinkParams] = strings.Split(item.Value, ",")
return nil
},
conf.DefaultRole: func(item *model.SettingItem) error {
v := strings.TrimSpace(item.Value)
if v == "" {
return nil
}
id, err := strconv.Atoi(v)
if err != nil {
return errors.WithStack(err)
}
_, err = GetRole(uint(id))
return err
},
}
func RegisterSettingItemHook(key string, hook SettingItemHook) {

View File

@@ -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
}

View File

@@ -2,9 +2,11 @@ package op
import (
"fmt"
"strconv"
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
@@ -15,6 +17,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 {
@@ -46,6 +52,23 @@ func GetRoleByName(name string) (*model.Role, error) {
return r, err
}
func GetDefaultRoleID() int {
item, err := GetSettingItemByKey(conf.DefaultRole)
if err == nil && item != nil && item.Value != "" {
if id, err := strconv.Atoi(item.Value); err == nil && id != 0 {
return id
}
if r, err := db.GetRoleByName(item.Value); err == nil {
return int(r.ID)
}
}
var r model.Role
if err := db.GetDb().Where("`default` = ?", true).First(&r).Error; err == nil {
return int(r.ID)
}
return int(model.GUEST)
}
func GetRolesByUserID(userID uint) ([]model.Role, error) {
user, err := GetUserById(userID)
if err != nil {
@@ -88,7 +111,21 @@ func CreateRole(r *model.Role) error {
}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.CreateRole(r)
if err := db.CreateRole(r); err != nil {
return err
}
if r.Default {
roleCache.Clear()
item, err := GetSettingItemByKey(conf.DefaultRole)
if err != nil {
return err
}
item.Value = strconv.Itoa(int(r.ID))
if err := SaveSettingItem(item); err != nil {
return err
}
}
return nil
}
func UpdateRole(r *model.Role) error {
@@ -127,7 +164,21 @@ func UpdateRole(r *model.Role) error {
//}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.UpdateRole(r)
if err := db.UpdateRole(r); err != nil {
return err
}
if r.Default {
roleCache.Clear()
item, err := GetSettingItemByKey(conf.DefaultRole)
if err != nil {
return err
}
item.Value = strconv.Itoa(int(r.ID))
if err := SaveSettingItem(item); err != nil {
return err
}
}
return nil
}
func DeleteRole(id uint) error {

View File

@@ -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
}

View File

@@ -9,8 +9,10 @@ import (
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
@@ -89,6 +91,35 @@ func loginHash(c *gin.Context, req *LoginReq) {
loginCache.Del(ip)
}
type RegisterReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Register a new user
func Register(c *gin.Context) {
if !setting.GetBool(conf.AllowRegister) {
common.ErrorStrResp(c, "registration is disabled", 403)
return
}
var req RegisterReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user := &model.User{
Username: req.Username,
Role: model.Roles{op.GetDefaultRoleID()},
Authn: "[]",
}
user.SetPassword(req.Password)
if err := op.CreateUser(user); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}
type UserResp struct {
model.User
Otp bool `json:"otp"`

View File

@@ -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)),
})
}

View File

@@ -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,
})
}

View File

@@ -44,7 +44,7 @@ func GetRole(c *gin.Context) {
func CreateRole(c *gin.Context) {
var req model.Role
if err := c.ShouldBind(&req); err != nil {
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
@@ -56,8 +56,14 @@ func CreateRole(c *gin.Context) {
}
func UpdateRole(c *gin.Context) {
var req model.Role
if err := c.ShouldBind(&req); err != nil {
var req struct {
ID uint `json:"id"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
PermissionScopes []model.PermissionEntry `json:"permission_scopes"`
Default *bool `json:"default"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
@@ -74,7 +80,13 @@ func UpdateRole(c *gin.Context) {
case "guest":
req.Name = "guest"
}
if err := op.UpdateRole(&req); err != nil {
role.Name = req.Name
role.Description = req.Description
role.PermissionScopes = req.PermissionScopes
if req.Default != nil {
role.Default = *req.Default
}
if err := op.UpdateRole(role); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)

View File

@@ -14,6 +14,21 @@ import (
"github.com/gin-gonic/gin"
)
func getRoleOptions() string {
roles, _, err := op.GetRoles(1, model.MaxInt)
if err != nil {
return ""
}
names := make([]string, 0, len(roles))
for _, r := range roles {
if r.Name == "admin" || r.Name == "guest" {
continue
}
names = append(names, r.Name)
}
return strings.Join(names, ",")
}
func ResetToken(c *gin.Context) {
token := random.Token()
item := model.SettingItem{Key: "token", Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE}
@@ -34,6 +49,17 @@ func GetSetting(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
if item.Key == conf.DefaultRole {
copy := *item
copy.Options = getRoleOptions()
if id, err := strconv.Atoi(copy.Value); err == nil {
if r, err := op.GetRole(uint(id)); err == nil {
copy.Value = r.Name
}
}
common.SuccessResp(c, copy)
return
}
common.SuccessResp(c, item)
} else {
items, err := op.GetSettingItemInKeys(strings.Split(keys, ","))
@@ -41,6 +67,17 @@ func GetSetting(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
for i := range items {
if items[i].Key == conf.DefaultRole {
if id, err := strconv.Atoi(items[i].Value); err == nil {
if r, err := op.GetRole(uint(id)); err == nil {
items[i].Value = r.Name
}
}
items[i].Options = getRoleOptions()
break
}
}
common.SuccessResp(c, items)
}
}
@@ -51,6 +88,22 @@ func SaveSettings(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
for i := range req {
if req[i].Key == conf.DefaultRole {
role, err := op.GetRoleByName(req[i].Value)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if role.Name == "admin" || role.Name == "guest" {
common.ErrorStrResp(c, "cannot set admin or guest as default role", 400)
return
}
req[i].Value = strconv.Itoa(int(role.ID))
}
}
if err := op.SaveSettingItems(req); err != nil {
common.ErrorResp(c, err, 500)
} else {
@@ -88,6 +141,17 @@ func ListSettings(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
for i := range settings {
if settings[i].Key == conf.DefaultRole {
if id, err := strconv.Atoi(settings[i].Value); err == nil {
if r, err := op.GetRole(uint(id)); err == nil {
settings[i].Value = r.Name
}
}
settings[i].Options = getRoleOptions()
break
}
}
common.SuccessResp(c, settings)
}

View File

@@ -36,6 +36,9 @@ func CreateUser(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
if len(req.Role) == 0 {
req.Role = model.Roles{op.GetDefaultRoleID()}
}
if req.IsAdmin() || req.IsGuest() {
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
return
@@ -67,10 +70,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 == "" {

View File

@@ -61,6 +61,7 @@ func Init(e *gin.Engine) {
api.POST("/auth/login", handles.Login)
api.POST("/auth/login/hash", handles.LoginHash)
api.POST("/auth/login/ldap", handles.LoginLdap)
api.POST("/auth/register", handles.Register)
auth.GET("/me", handles.CurrentUser)
auth.POST("/me/update", handles.UpdateCurrent)
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
@@ -92,6 +93,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 +173,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 +219,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

View File

@@ -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()

View File

@@ -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

View File

@@ -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 += "/"
}
@@ -734,7 +833,7 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu
func makePropstatResponse(href string, pstats []Propstat) *response {
resp := response{
Href: []string{(&url.URL{Path: href}).EscapedPath()},
Href: []string{href},
Propstat: make([]propstat, 0, len(pstats)),
}
for _, p := range pstats {