Compare commits

..

23 Commits

Author SHA1 Message Date
千石
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
千石
6b2d81eede feat(user): enhance path management and role handling (#9249)
- Add `GetUsersByRole` function for fetching users by role.
- Introduce `GetAllBasePathsFromRoles` to aggregate paths from roles.
- Refine path handling in `pkg/utils/path.go` for normalization.
- Comment out base path prefix updates to simplify role operations.
2025-08-06 16:31:36 +08:00
千石
85fe4e5bb3 feat(alist_v3): add IntSlice type for JSON unmarshalling (#9247)
- Add `IntSlice` type to handle both single int and array in JSON.
- Modify `MeResp` struct to use `IntSlice` for `Role` field.
- Import `encoding/json` for JSON operations.
2025-08-04 12:02:45 +08:00
千石
52da07e8a7 feat(123_open): add new driver support for 123 Open (#9246)
- Implement new driver for 123 Open service, enabling file operations
  such as listing, uploading, moving, and removing files.
- Introduce token management for authentication and authorization.
- Add API integration for various file operations and actions.
- Include utility functions for handling API requests and responses.
- Register the new driver in the existing drivers' list.
2025-08-04 11:56:57 +08:00
Sky_slience
46de9e9ebb fix(driver): 123 download and modify request headers on the frontend (#9236)
Co-authored-by: Sky_slience <Skyslience@spdzy.com>
2025-08-03 20:00:09 +08:00
千石
ae90fb579b feat(log): enhance log formatter to respect NO_COLOR env variable (#9239)
- Adjust log formatter to disable colors when NO_COLOR or ALIST_NO_COLOR
  environment variables are set.
- Reorganize formatter settings for better readability.
2025-08-03 09:26:23 +08:00
Sky_slience
394a18cbd9 Fix 123 download (#9235)
* fix(driver): handle additional HTTP status code 210 for URL redirection

* fix(driver): 123 download url error

---------

Co-authored-by: Sky_slience <Skyslience@spdzy.com>
2025-07-30 16:55:32 +08:00
千石
280960ce3e feat(user-db): enhance user management with role-based queries (allow-edit-role-guest) (#9234)
- Add `GetUsersByRole` function to fetch users based on their roles.
- Extend `UpdateUserBasePathPrefix` to accept optional user lists.
- Ensure path cleaning in `UpdateUserBasePathPrefix` for consistency.
- Integrate guest role fetching in `auth.go` middleware.
- Utilize `GetUsersByRole` in `role.go` for base path modifications.
- Remove redundant line in `role.go` role modification logic.
2025-07-30 13:15:35 +08:00
Sky_slience
74332e91fb feat(ui): add new UI configuration option to settings (#9233)
* feat(ui): add new UI configuration option to settings

* fix(ui): disable new UI feature by default

---------

Co-authored-by: Sky_slience <Skyslience@spdzy.com>
2025-07-30 12:22:02 +08:00
Sky_slience
540d6c7064 fix(meta): update OAuth token URL and improve default client credentials (#9231) 2025-07-30 10:48:33 +08:00
千石
55b2bb6b80 feat(user-management): Enhance admin management and role handling 2025-07-29 19:45:28 +08:00
qianshi
d5df6fa4cf Merge branch 'main' into feat/allow-edit-role-guest 2025-07-29 19:13:01 +08:00
千石
3353055482 Update Dockerfile.ci (#9230)
chore(docker): Update base image from alpine:edge to alpine:3.20.7 in Dockerfile.ci
2025-07-29 18:35:47 +08:00
千石
4d7c2a09ce docs(README): Add API documentation links across multiple languages (#9225)
- Add API documentation section to `README.md` with link to Apifox
- Add API documentation section to `README_ja.md` with Japanese translation and link to Apifox
- Add API documentation section to `README_cn.md` with Chinese translation and link to Apifox
2025-07-29 09:42:34 +08:00
qianshi
5b8c26510b feat(user-management): Enhance admin management and role handling
- Add `CountEnabledAdminsExcluding` function to count enabled admins excluding a specific user.
- Implement `CountUsersByRoleAndEnabledExclude` in `internal/db/user.go` to support exclusion logic.
- Refactor role handling with switch-case for better readability in `server/handles/role.go`.
- Ensure at least one enabled admin remains when disabling an admin in `server/handles/user.go`.
- Maintain guest role name consistency when updating roles in `internal/op/role.go`.
2025-07-28 23:07:07 +08:00
千石
91cc7529a0 feat(user/role/storage): enhance user and storage operations with additional validations (#9223)
- Update `CreateUser` to adjust `BasePath` based on user roles and clean paths.
- Modify `UpdateUser` to incorporate role-based path changes.
- Add validation in `CreateStorage` and `UpdateStorage` to prevent root mount path.
- Prevent changes to admin user's role and username in user handler.
- Update `UpdateRole` to modify user base paths when role paths change, and clear user cache accordingly.
- Import `errors` package to handle error messages.
2025-07-27 22:25:45 +08:00
千石
f61d13d433 refactor(convert_role): Improve role conversion logic for legacy formats (#9219)
- Add new imports: `database/sql`, `encoding/json`, and `conf` package in `convert_role.go`.
- Simplify permission entry initialization by removing redundant struct formatting.
- Update error logging messages for better clarity.
- Replace `op.GetUsers` with direct database access for fetching user roles.
- Implement role update logic using `rawDb` and handle legacy int role conversion.
- Count the number of users whose roles are updated and log completion.
- Introduce `IsLegacyRoleDetected` function to check for legacy role formats.
- Modify `cmd/common.go` to invoke role conversion if legacy format is detected.
2025-07-26 15:20:08 +08:00
千石
00120cba27 feat: enhance permission control and label management (#9215)
* 标签管理

* pr检查优化

* feat(role): Implement role management functionality

- Add role management routes in `server/router.go` for listing, getting, creating, updating, and deleting roles
- Introduce `initRoles()` in `internal/bootstrap/data/data.go` for initializing roles during bootstrap
- Create `internal/op/role.go` to handle role operations including caching and singleflight
- Implement role handler functions in `server/handles/role.go` for API responses
- Define database operations for roles in `internal/db/role.go`
- Extend `internal/db/db.go` for role model auto-migration
- Design `internal/model/role.go` to represent role structure with ID, name, description, base path, and permissions
- Initialize default roles (`admin` and `guest`) in `internal/bootstrap/data/role.go` during startup

* refactor(user roles): Support multiple roles for users

- Change the `Role` field type from `int` to `[]int` in `drivers/alist_v3/types.go` and `drivers/quqi/types.go`.
- Update the `Role` field in `internal/model/user.go` to use a new `Roles` type with JSON and database support.
- Modify `IsGuest` and `IsAdmin` methods to check for roles using `Contains` method.
- Update `GetUserByRole` method in `internal/db/user.go` to handle multiple roles.
- Add `roles.go` to define a new `Roles` type with JSON marshalling and scanning capabilities.
- Adjust code in `server/handles/user.go` to compare roles with `utils.SliceEqual`.
- Change role initialization for users in `internal/bootstrap/data/dev.go` and `internal/bootstrap/data/user.go`.
- Update `Role` handling in `server/handles/task.go`, `server/handles/ssologin.go`, and `server/handles/ldap_login.go`.

* feat(user/role): Add path limit check for user and role permissions

- Add new permission bit for checking path limits in `user.go`
- Implement `CheckPathLimit` method in `User` struct to validate path access
- Modify `JoinPath` method in `User` to enforce path limit checks
- Update `role.go` to include path limit logic in `Role` struct
- Document new permission bit in `Role` and `User` comments for clarity

* feat(permission): Add role-based permission handling

- Introduce `role_perm.go` for managing user permissions based on roles.
- Implement `HasPermission` and `MergeRolePermissions` functions.
- Update `webdav.go` to utilize role-based permissions instead of direct user checks.
- Modify `fsup.go` to integrate `CanAccessWithRoles` function.
- Refactor `fsread.go` to use `common.HasPermission` for permission validation.
- Adjust `fsmanage.go` for role-based access control checks.
- Enhance `ftp.go` and `sftp.go` to manage FTP access via roles.
- Update `fsbatch.go` to employ `MergeRolePermissions` for batch operations.
- Replace direct user permission checks with role-based permission handling across various modules.

* refactor(user): Replace integer role values with role IDs

- Change `GetAdmin()` and `GetGuest()` functions to retrieve role by name and use role ID.
- Add patch for version `v3.45.2` to convert legacy integer roles to role IDs.
- Update `dev.go` and `user.go` to use role IDs instead of integer values for roles.
- Remove redundant code in `role.go` related to guest role creation.
- Modify `ssologin.go` and `ldap_login.go` to set user roles to nil instead of using integer roles.
- Introduce `convert_roles.go` to handle conversion of legacy roles and ensure role existence in the database.

* feat(role_perm): implement support for multiple base paths for roles

- Modify role permission checks to support multiple base paths
- Update role creation and update functions to handle multiple base paths
- Add migration script to convert old base_path to base_paths
- Define new Paths type for handling multiple paths in the model
- Adjust role model to replace BasePath with BasePaths
- Update existing patches to handle roles with multiple base paths
- Update bootstrap data to reflect the new base_paths field

* feat(role): Restrict modifications to default roles (admin and guest)

- Add validation to prevent changes to "admin" and "guest" roles in `UpdateRole` and `DeleteRole` functions.
- Introduce `ErrChangeDefaultRole` error in `internal/errs/role.go` to standardize error messaging.
- Update role-related API handlers in `server/handles/role.go` to enforce the new restriction.
- Enhance comments in `internal/bootstrap/data/role.go` to clarify the significance of default roles.
- Ensure consistent error responses for unauthorized role modifications across the application.

* 🔄 **refactor(role): Enhance role permission handling**

- Replaced `BasePaths` with `PermissionPaths` in `Role` struct for better permission granularity.
- Introduced JSON serialization for `PermissionPaths` using `RawPermission` field in `Role` struct.
- Implemented `BeforeSave` and `AfterFind` GORM hooks for handling `PermissionPaths` serialization.
- Refactored permission calculation logic in `role_perm.go` to work with `PermissionPaths`.
- Updated role creation logic to initialize `PermissionPaths` for `admin` and `guest` roles.
- Removed deprecated `CheckPathLimit` method from `Role` struct.

* fix(model/user/role): update permission settings for admin and role

- Change `RawPermission` field in `role.go` to hide JSON representation
- Update `Permission` field in `user.go` to `0xFFFF` for full access
- Modify `PermissionScopes` in `role.go` to `0xFFFF` for enhanced permissions

* 🔒 feat(role-permissions): Enhance role-based access control

- Introduce `canReadPathByRole` function in `role_perm.go` to verify path access based on user roles
- Modify `CanAccessWithRoles` to include role-based path read check
- Add `RoleNames` and `Permissions` to `UserResp` struct in `auth.go` for enhanced user role and permission details
- Implement role details aggregation in `auth.go` to populate `RoleNames` and `Permissions`
- Update `User` struct in `user.go` to include `RolesDetail` for more detailed role information
- Enhance middleware in `auth.go` to load and verify detailed role information for users
- Move `guest` user initialization logic in `user.go` to improve code organization and avoid repetition

* 🔒 fix(permissions): Add permission checks for archive operations

- Add `MergeRolePermissions` and `HasPermission` checks to validate user access for reading archives
- Ensure users have `PermReadArchives` before proceeding with `GetNearestMeta` in specific archive paths
- Implement permission checks for decompress operations, requiring `PermDecompress` for source paths
- Return `PermissionDenied` errors with 403 status if user lacks necessary permissions

* 🔒 fix(server): Add permission check for offline download

- Add permission merging logic for user roles
- Check user has permission for offline download addition
- Return error response with "permission denied" if check fails

*  feat(role-permission): Implement path-based role permission checks

- Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission.
- Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control.
- Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion).
- Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions.
- Update error handling to return `PermissionDenied` if the path validation fails.
- Import `errs` package in `offline_download` for consistent error responses.

*  feat(role-permission): Implement path-based role permission checks

- Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission.
- Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control.
- Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion).
- Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions.
- Update error handling to return `PermissionDenied` if the path validation fails.
- Import `errs` package in `offline_download` for consistent error responses.

* ♻️ refactor(access-control): Update access control logic to use role-based checks

- Remove deprecated logic from `CanAccess` function in `check.go`, replacing it with `CanAccessWithRoles` for improved role-based access control.
- Modify calls in `search.go` to use `CanAccessWithRoles` for more precise handling of permissions.
- Update `fsread.go` to utilize `CanAccessWithRoles`, ensuring accurate access validation based on user roles.
- Simplify import statements in `check.go` by removing unused packages to clean up the codebase.

*  feat(fs): Improve visibility logic for hidden files

- Import `server/common` package to handle permissions more robustly
- Update `whetherHide` function to use `MergeRolePermissions` for user-specific path permissions
- Replace direct user checks with `HasPermission` for `PermSeeHides`
- Enhance logic to ensure `nil` user cases are handled explicitly

* 标签管理

* feat(db/auth/user): Enhance role handling and clean permission paths

- Comment out role modification checks in `server/handles/user.go` to allow flexible role changes.
- Improve permission path handling in `server/handles/auth.go` by normalizing and deduplicating paths.
- Introduce `addedPaths` map in `CurrentUser` to prevent duplicate permissions.

* feat(storage/db): Implement role permissions path prefix update

- Add `UpdateRolePermissionsPathPrefix` function in `role.go` to update role permissions paths.
- Modify `storage.go` to call the new function when the mount path is renamed.
- Introduce path cleaning and prefix matching logic for accurate path updates.
- Ensure roles are updated only if their permission scopes are modified.
- Handle potential errors with informative messages during database operations.

* feat(role-migration): Implement role conversion and introduce NEWGENERAL role

- Add `NEWGENERAL` to the roles enumeration in `user.go`
- Create new file `convert_role.go` for migrating legacy roles to new model
- Implement `ConvertLegacyRoles` function to handle role conversion with permission scopes
- Add `convert_role.go` patch to `all.go` under version `v3.46.0`

* feat(role/auth): Add role retrieval by user ID and update path prefixes

- Add `GetRolesByUserID` function for efficient role retrieval by user ID
- Implement `UpdateUserBasePathPrefix` to update user base paths
- Modify `UpdateRolePermissionsPathPrefix` to return modified role IDs
- Update `auth.go` middleware to use the new role retrieval function
- Refresh role and user caches upon path prefix updates to maintain consistency

---------

Co-authored-by: Leslie-Xy <540049476@qq.com>
2025-07-26 09:51:59 +08:00
Sakana
5e15a360b7 feat(github_releases): concurrently request the GitHub API (#9211) 2025-07-24 15:30:12 +08:00
alist666
2bdc5bef9e Merge pull request #9207 from AlistGo/fix-aliyundirve
fix: update DriveId assignment to use DeviceID from Addition struct
2025-07-17 13:21:32 +08:00
AlistDev
13ea1c1405 fix: restore user-agent header in HTTP requests 2025-07-16 20:39:05 +08:00
AlistDev
fd41186679 fix: update DriveId assignment to use DeviceID from Addition struct 2025-07-14 23:04:40 +08:00
84 changed files with 3686 additions and 304 deletions

View File

@@ -1,4 +1,4 @@
FROM alpine:edge
FROM alpine:3.20.7
ARG TARGETPLATFORM
ARG INSTALL_FFMPEG=false

View File

@@ -101,6 +101,10 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
<https://alistgo.com/>
## API Documentation (via Apifox):
<https://alist-public.apifox.cn/>
## Demo
<https://al.nn.ci>

View File

@@ -99,6 +99,10 @@
<https://alistgo.com/zh/>
## API 文档(通过 Apifox 提供)
<https://alist-public.apifox.cn/>
## Demo
<https://al.nn.ci>

View File

@@ -100,6 +100,10 @@
<https://alistgo.com/>
## APIドキュメントApifox 提供)
<https://alist-public.apifox.cn/>
## デモ
<https://al.nn.ci>

View File

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

View File

@@ -161,12 +161,12 @@ func (d *Pan123) login() error {
}
res, err := base.RestyClient.R().
SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)-alist",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
//"user-agent": "Dart/2.19(dart:io)-alist",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
"user-agent": base.UserAgent,
}).
SetBody(body).Post(SignIn)
if err != nil {
@@ -202,7 +202,7 @@ do:
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,

191
drivers/123_open/api.go Normal file
View File

@@ -0,0 +1,191 @@
package _123Open
import (
"fmt"
"github.com/go-resty/resty/v2"
"net/http"
)
const (
// baseurl
ApiBaseURL = "https://open-api.123pan.com"
// auth
ApiToken = "/api/v1/access_token"
// file list
ApiFileList = "/api/v2/file/list"
// direct link
ApiGetDirectLink = "/api/v1/direct-link/url"
// mkdir
ApiMakeDir = "/upload/v1/file/mkdir"
// remove
ApiRemove = "/api/v1/file/trash"
// upload
ApiUploadDomainURL = "/upload/v2/file/domain"
ApiSingleUploadURL = "/upload/v2/file/single/create"
ApiCreateUploadURL = "/upload/v2/file/create"
ApiUploadSliceURL = "/upload/v2/file/slice"
ApiUploadCompleteURL = "/upload/v2/file/upload_complete"
// move
ApiMove = "/api/v1/file/move"
// rename
ApiRename = "/api/v1/file/name"
)
type Response[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
type TokenResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data TokenData `json:"data"`
}
type TokenData struct {
AccessToken string `json:"accessToken"`
ExpiredAt string `json:"expiredAt"`
}
type FileListResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data FileListData `json:"data"`
}
type FileListData struct {
LastFileId int64 `json:"lastFileId"`
FileList []File `json:"fileList"`
}
type DirectLinkResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data DirectLinkData `json:"data"`
}
type DirectLinkData struct {
URL string `json:"url"`
}
type MakeDirRequest struct {
Name string `json:"name"`
ParentID int64 `json:"parentID"`
}
type MakeDirResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data MakeDirData `json:"data"`
}
type MakeDirData struct {
DirID int64 `json:"dirID"`
}
type RemoveRequest struct {
FileIDs []int64 `json:"fileIDs"`
}
type UploadCreateResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data UploadCreateData `json:"data"`
}
type UploadCreateData struct {
FileID int64 `json:"fileId"`
Reuse bool `json:"reuse"`
PreuploadID string `json:"preuploadId"`
SliceSize int64 `json:"sliceSize"`
Servers []string `json:"servers"`
}
type UploadUrlResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data UploadUrlData `json:"data"`
}
type UploadUrlData struct {
PresignedURL string `json:"presignedUrl"`
}
type UploadCompleteResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data UploadCompleteData `json:"data"`
}
type UploadCompleteData struct {
FileID int `json:"fileID"`
Completed bool `json:"completed"`
}
func (d *Open123) Request(endpoint string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
client := resty.New()
token, err := d.tm.getToken()
if err != nil {
return nil, err
}
req := client.R().
SetHeader("Authorization", "Bearer "+token).
SetHeader("Platform", "open_platform").
SetHeader("Content-Type", "application/json").
SetResult(result)
if setup != nil {
setup(req)
}
switch method {
case http.MethodGet:
return req.Get(ApiBaseURL + endpoint)
case http.MethodPost:
return req.Post(ApiBaseURL + endpoint)
case http.MethodPut:
return req.Put(ApiBaseURL + endpoint)
default:
return nil, fmt.Errorf("unsupported method: %s", method)
}
}
func (d *Open123) RequestTo(fullURL string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
client := resty.New()
token, err := d.tm.getToken()
if err != nil {
return nil, err
}
req := client.R().
SetHeader("Authorization", "Bearer "+token).
SetHeader("Platform", "open_platform").
SetHeader("Content-Type", "application/json").
SetResult(result)
if setup != nil {
setup(req)
}
switch method {
case http.MethodGet:
return req.Get(fullURL)
case http.MethodPost:
return req.Post(fullURL)
case http.MethodPut:
return req.Put(fullURL)
default:
return nil, fmt.Errorf("unsupported method: %s", method)
}
}

277
drivers/123_open/driver.go Normal file
View File

@@ -0,0 +1,277 @@
package _123Open
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"net/http"
"strconv"
)
type Open123 struct {
model.Storage
Addition
UploadThread int
tm *tokenManager
}
func (d *Open123) Config() driver.Config {
return config
}
func (d *Open123) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Open123) Init(ctx context.Context) error {
d.tm = newTokenManager(d.ClientID, d.ClientSecret)
if _, err := d.tm.getToken(); err != nil {
return fmt.Errorf("token 初始化失败: %w", err)
}
return nil
}
func (d *Open123) Drop(ctx context.Context) error {
return nil
}
func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
parentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)
if err != nil {
return nil, err
}
fileLastId := int64(0)
var results []File
for fileLastId != -1 {
files, err := d.getFiles(parentFileId, 100, fileLastId)
if err != nil {
return nil, err
}
for _, f := range files.Data.FileList {
if f.Trashed == 0 {
results = append(results, f)
}
}
fileLastId = files.Data.LastFileId
}
objs := make([]model.Obj, 0, len(results))
for _, f := range results {
objs = append(objs, f)
}
return objs, nil
}
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if file.IsDir() {
return nil, errs.LinkIsDir
}
fileID := file.GetID()
var result DirectLinkResp
url := fmt.Sprintf("%s?fileID=%s", ApiGetDirectLink, fileID)
_, err := d.Request(url, http.MethodGet, nil, &result)
if err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("get link failed: %s", result.Message)
}
return &model.Link{
URL: result.Data.URL,
}, nil
}
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
parentID, err := strconv.ParseInt(parentDir.GetID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid parent ID: %w", err)
}
var result MakeDirResp
reqBody := MakeDirRequest{
Name: dirName,
ParentID: parentID,
}
_, err = d.Request(ApiMakeDir, http.MethodPost, func(r *resty.Request) {
r.SetBody(reqBody)
}, &result)
if err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("mkdir failed: %s", result.Message)
}
newDir := File{
FileId: result.Data.DirID,
FileName: dirName,
Type: 1,
ParentFileId: int(parentID),
Size: 0,
Trashed: 0,
}
return newDir, nil
}
func (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid src file ID: %w", err)
}
dstID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid dest dir ID: %w", err)
}
var result Response[any]
reqBody := map[string]interface{}{
"fileIDs": []int64{srcID},
"toParentFileID": dstID,
}
_, err = d.Request(ApiMove, http.MethodPost, func(r *resty.Request) {
r.SetBody(reqBody)
}, &result)
if err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("move failed: %s", result.Message)
}
files, err := d.getFiles(dstID, 100, 0)
if err != nil {
return nil, fmt.Errorf("move succeed but failed to get target dir: %w", err)
}
for _, f := range files.Data.FileList {
if f.FileId == srcID {
return f, nil
}
}
return nil, fmt.Errorf("move succeed but file not found in target dir")
}
func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid file ID: %w", err)
}
var result Response[any]
reqBody := map[string]interface{}{
"fileId": srcID,
"fileName": newName,
}
_, err = d.Request(ApiRename, http.MethodPut, func(r *resty.Request) {
r.SetBody(reqBody)
}, &result)
if err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("rename failed: %s", result.Message)
}
parentID := 0
if file, ok := srcObj.(File); ok {
parentID = file.ParentFileId
}
files, err := d.getFiles(int64(parentID), 100, 0)
if err != nil {
return nil, fmt.Errorf("rename succeed but failed to get parent dir: %w", err)
}
for _, f := range files.Data.FileList {
if f.FileId == srcID {
return f, nil
}
}
return nil, fmt.Errorf("rename succeed but file not found in parent dir")
}
func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return nil, errs.NotSupport
}
func (d *Open123) Remove(ctx context.Context, obj model.Obj) error {
idStr := obj.GetID()
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid file ID: %w", err)
}
var result Response[any]
reqBody := RemoveRequest{
FileIDs: []int64{id},
}
_, err = d.Request(ApiRemove, http.MethodPost, func(r *resty.Request) {
r.SetBody(reqBody)
}, &result)
if err != nil {
return err
}
if result.Code != 0 {
return fmt.Errorf("remove failed: %s", result.Message)
}
return nil
}
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
up = model.UpdateProgressWithRange(up, 50, 100)
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return err
}
}
createResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)
if err != nil {
return err
}
if createResp.Data.Reuse {
return nil
}
return d.Upload(ctx, file, parentFileId, createResp, up)
}
func (d *Open123) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
return nil, errs.NotSupport
}
func (d *Open123) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
return nil, errs.NotSupport
}
func (d *Open123) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
return nil, errs.NotSupport
}
func (d *Open123) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
return nil, errs.NotSupport
}
//func (d *Open123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Open123)(nil)

33
drivers/123_open/meta.go Normal file
View File

@@ -0,0 +1,33 @@
package _123Open
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
ClientID string `json:"client_id" required:"true" label:"Client ID"`
ClientSecret string `json:"client_secret" required:"true" label:"Client Secret"`
}
var config = driver.Config{
Name: "123 Open",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Open123{}
})
}

85
drivers/123_open/token.go Normal file
View File

@@ -0,0 +1,85 @@
package _123Open
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
const tokenURL = ApiBaseURL + ApiToken
type tokenManager struct {
clientID string
clientSecret string
mu sync.Mutex
accessToken string
expireTime time.Time
}
func newTokenManager(clientID, clientSecret string) *tokenManager {
return &tokenManager{
clientID: clientID,
clientSecret: clientSecret,
}
}
func (tm *tokenManager) getToken() (string, error) {
tm.mu.Lock()
defer tm.mu.Unlock()
if tm.accessToken != "" && time.Now().Before(tm.expireTime.Add(-5*time.Minute)) {
return tm.accessToken, nil
}
reqBody := map[string]string{
"clientID": tm.clientID,
"clientSecret": tm.clientSecret,
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", tokenURL, bytes.NewBuffer(body))
if err != nil {
return "", err
}
req.Header.Set("Platform", "open_platform")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result TokenResp
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if result.Code != 0 {
return "", fmt.Errorf("get token failed: %s", result.Message)
}
tm.accessToken = result.Data.AccessToken
expireAt, err := time.Parse(time.RFC3339, result.Data.ExpiredAt)
if err != nil {
return "", fmt.Errorf("parse expire time failed: %w", err)
}
tm.expireTime = expireAt
return tm.accessToken, nil
}
func (tm *tokenManager) buildHeaders() (http.Header, error) {
token, err := tm.getToken()
if err != nil {
return nil, err
}
header := http.Header{}
header.Set("Authorization", "Bearer "+token)
header.Set("Platform", "open_platform")
header.Set("Content-Type", "application/json")
return header, nil
}

70
drivers/123_open/types.go Normal file
View File

@@ -0,0 +1,70 @@
package _123Open
import (
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"time"
)
type File struct {
FileName string `json:"filename"`
Size int64 `json:"size"`
CreateAt string `json:"createAt"`
UpdateAt string `json:"updateAt"`
FileId int64 `json:"fileId"`
Type int `json:"type"`
Etag string `json:"etag"`
S3KeyFlag string `json:"s3KeyFlag"`
ParentFileId int `json:"parentFileId"`
Category int `json:"category"`
Status int `json:"status"`
Trashed int `json:"trashed"`
}
func (f File) GetID() string {
return fmt.Sprint(f.FileId)
}
func (f File) GetName() string {
return f.FileName
}
func (f File) GetSize() int64 {
return f.Size
}
func (f File) IsDir() bool {
return f.Type == 1
}
func (f File) GetModified() string {
return f.UpdateAt
}
func (f File) GetThumb() string {
return ""
}
func (f File) ModTime() time.Time {
t, err := time.Parse("2006-01-02 15:04:05", f.UpdateAt)
if err != nil {
return time.Time{}
}
return t
}
func (f File) CreateTime() time.Time {
t, err := time.Parse("2006-01-02 15:04:05", f.CreateAt)
if err != nil {
return time.Time{}
}
return t
}
func (f File) GetHash() utils.HashInfo {
return utils.NewHashInfo(utils.MD5, f.Etag)
}
func (f File) GetPath() string {
return ""
}

282
drivers/123_open/upload.go Normal file
View File

@@ -0,0 +1,282 @@
package _123Open
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
"golang.org/x/sync/errgroup"
"io"
"mime/multipart"
"net/http"
"runtime"
"strconv"
"time"
)
func (d *Open123) create(parentFileID int64, filename, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {
var resp UploadCreateResp
_, err := d.Request(ApiCreateUploadURL, http.MethodPost, func(req *resty.Request) {
body := base.Json{
"parentFileID": parentFileID,
"filename": filename,
"etag": etag,
"size": size,
}
if duplicate > 0 {
body["duplicate"] = duplicate
}
if containDir {
body["containDir"] = true
}
req.SetBody(body)
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (d *Open123) GetUploadDomains() ([]string, error) {
var resp struct {
Code int `json:"code"`
Message string `json:"message"`
Data []string `json:"data"`
}
_, err := d.Request(ApiUploadDomainURL, http.MethodGet, nil, &resp)
if err != nil {
return nil, err
}
if resp.Code != 0 {
return nil, fmt.Errorf("get upload domain failed: %s", resp.Message)
}
return resp.Data, nil
}
func (d *Open123) UploadSingle(ctx context.Context, createResp *UploadCreateResp, file model.FileStreamer, parentID int64) error {
domain := createResp.Data.Servers[0]
etag := file.GetHash().GetHash(utils.MD5)
if len(etag) < utils.MD5.Width {
_, _, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)
if err != nil {
return err
}
}
reader, err := file.RangeRead(http_range.Range{Start: 0, Length: file.GetSize()})
if err != nil {
return err
}
reader = driver.NewLimitedUploadStream(ctx, reader)
var b bytes.Buffer
mw := multipart.NewWriter(&b)
mw.WriteField("parentFileID", fmt.Sprint(parentID))
mw.WriteField("filename", file.GetName())
mw.WriteField("etag", etag)
mw.WriteField("size", fmt.Sprint(file.GetSize()))
fw, _ := mw.CreateFormFile("file", file.GetName())
_, err = io.Copy(fw, reader)
mw.Close()
req, err := http.NewRequestWithContext(ctx, "POST", domain+ApiSingleUploadURL, &b)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
req.Header.Set("Platform", "open_platform")
req.Header.Set("Content-Type", mw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
FileID int64 `json:"fileID"`
Completed bool `json:"completed"`
} `json:"data"`
}
body, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("unmarshal response error: %v, body: %s", err, string(body))
}
if result.Code != 0 {
return fmt.Errorf("upload failed: %s", result.Message)
}
if !result.Data.Completed || result.Data.FileID == 0 {
return fmt.Errorf("upload incomplete or missing fileID")
}
return nil
}
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, parentID int64, createResp *UploadCreateResp, up driver.UpdateProgress) error {
if cacher, ok := file.(interface{ CacheFullInTempFile() (model.File, error) }); ok {
if _, err := cacher.CacheFullInTempFile(); err != nil {
return err
}
}
size := file.GetSize()
chunkSize := createResp.Data.SliceSize
uploadNums := (size + chunkSize - 1) / chunkSize
uploadDomain := createResp.Data.Servers[0]
if d.UploadThread <= 0 {
cpuCores := runtime.NumCPU()
threads := cpuCores * 2
if threads < 4 {
threads = 4
}
if threads > 16 {
threads = 16
}
d.UploadThread = threads
fmt.Printf("[Upload] Auto set upload concurrency: %d (CPU cores=%d)\n", d.UploadThread, cpuCores)
}
fmt.Printf("[Upload] File size: %d bytes, chunk size: %d bytes, total slices: %d, concurrency: %d\n",
size, chunkSize, uploadNums, d.UploadThread)
if size <= 1<<30 {
return d.UploadSingle(ctx, createResp, file, parentID)
}
if createResp.Data.Reuse {
up(100)
return nil
}
client := resty.New()
semaphore := make(chan struct{}, d.UploadThread)
threadG, _ := errgroup.WithContext(ctx)
var progressArr = make([]int64, uploadNums)
for partIndex := int64(0); partIndex < uploadNums; partIndex++ {
partIndex := partIndex
semaphore <- struct{}{}
threadG.Go(func() error {
defer func() { <-semaphore }()
offset := partIndex * chunkSize
length := min(chunkSize, size-offset)
partNumber := partIndex + 1
fmt.Printf("[Slice %d] Starting read from offset %d, length %d\n", partNumber, offset, length)
reader, err := file.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return fmt.Errorf("[Slice %d] RangeRead error: %v", partNumber, err)
}
buf := make([]byte, length)
n, err := io.ReadFull(reader, buf)
if err != nil && err != io.EOF {
return fmt.Errorf("[Slice %d] Read error: %v", partNumber, err)
}
buf = buf[:n]
hash := md5.Sum(buf)
sliceMD5Str := hex.EncodeToString(hash[:])
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
writer.WriteField("preuploadID", createResp.Data.PreuploadID)
writer.WriteField("sliceNo", strconv.FormatInt(partNumber, 10))
writer.WriteField("sliceMD5", sliceMD5Str)
partName := fmt.Sprintf("%s.part%d", file.GetName(), partNumber)
fw, _ := writer.CreateFormFile("slice", partName)
fw.Write(buf)
writer.Close()
resp, err := client.R().
SetHeader("Authorization", "Bearer "+d.tm.accessToken).
SetHeader("Platform", "open_platform").
SetHeader("Content-Type", writer.FormDataContentType()).
SetBody(body.Bytes()).
Post(uploadDomain + ApiUploadSliceURL)
if err != nil {
return fmt.Errorf("[Slice %d] Upload HTTP error: %v", partNumber, err)
}
if resp.StatusCode() != 200 {
return fmt.Errorf("[Slice %d] Upload failed with status: %s, resp: %s", partNumber, resp.Status(), resp.String())
}
progressArr[partIndex] = length
var totalUploaded int64 = 0
for _, v := range progressArr {
totalUploaded += v
}
if up != nil {
percent := float64(totalUploaded) / float64(size) * 100
up(percent)
}
fmt.Printf("[Slice %d] MD5: %s\n", partNumber, sliceMD5Str)
fmt.Printf("[Slice %d] Upload finished\n", partNumber)
return nil
})
}
if err := threadG.Wait(); err != nil {
return err
}
var completeResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Completed bool `json:"completed"`
FileID int64 `json:"fileID"`
} `json:"data"`
}
for {
reqBody := fmt.Sprintf(`{"preuploadID":"%s"}`, createResp.Data.PreuploadID)
req, err := http.NewRequestWithContext(ctx, "POST", uploadDomain+ApiUploadCompleteURL, bytes.NewBufferString(reqBody))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
req.Header.Set("Platform", "open_platform")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if err := json.Unmarshal(body, &completeResp); err != nil {
return fmt.Errorf("completion response unmarshal error: %v, body: %s", err, string(body))
}
if completeResp.Code != 0 {
return fmt.Errorf("completion API returned error code %d: %s", completeResp.Code, completeResp.Message)
}
if completeResp.Data.Completed && completeResp.Data.FileID != 0 {
fmt.Printf("[Upload] Upload completed successfully. FileID: %d\n", completeResp.Data.FileID)
break
}
time.Sleep(time.Second)
}
up(100)
return nil
}

20
drivers/123_open/util.go Normal file
View File

@@ -0,0 +1,20 @@
package _123Open
import (
"fmt"
"net/http"
)
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
var result FileListResp
url := fmt.Sprintf("%s?parentFileId=%d&limit=%d&lastFileId=%d", ApiFileList, parentFileId, limit, lastFileId)
_, err := d.Request(url, http.MethodGet, nil, &result)
if err != nil {
return nil, err
}
if result.Code != 0 {
return nil, fmt.Errorf("list error: %s", result.Message)
}
return &result, nil
}

View File

@@ -56,7 +56,7 @@ func (d *AListV3) Init(ctx context.Context) error {
if err != nil {
return err
}
if resp.Data.Role == model.GUEST {
if utils.SliceContains(resp.Data.Role, model.GUEST) {
u := d.Address + "/api/public/settings"
res, err := base.RestyClient.R().Get(u)
if err != nil {

View File

@@ -1,6 +1,7 @@
package alist_v3
import (
"encoding/json"
"time"
"github.com/alist-org/alist/v3/internal/model"
@@ -72,15 +73,15 @@ type LoginResp struct {
}
type MeResp struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
BasePath string `json:"base_path"`
Role int `json:"role"`
Disabled bool `json:"disabled"`
Permission int `json:"permission"`
SsoId string `json:"sso_id"`
Otp bool `json:"otp"`
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
BasePath string `json:"base_path"`
Role IntSlice `json:"role"`
Disabled bool `json:"disabled"`
Permission int `json:"permission"`
SsoId string `json:"sso_id"`
Otp bool `json:"otp"`
}
type ArchiveMetaReq struct {
@@ -168,3 +169,17 @@ type DecompressReq struct {
PutIntoNewDir bool `json:"put_into_new_dir"`
SrcDir string `json:"src_dir"`
}
type IntSlice []int
func (s *IntSlice) UnmarshalJSON(data []byte) error {
if len(data) > 0 && data[0] == '[' {
return json.Unmarshal(data, (*[]int)(s))
}
var single int
if err := json.Unmarshal(data, &single); err != nil {
return err
}
*s = []int{single}
return nil
}

View File

@@ -55,7 +55,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
if err != nil {
return err
}
d.DriveId = utils.Json.Get(res, "default_drive_id").ToString()
d.DriveId = d.Addition.DeviceID
d.UserID = utils.Json.Get(res, "user_id").ToString()
d.cron = cron.NewCron(time.Hour * 2)
d.cron.Do(func() {

View File

@@ -7,8 +7,8 @@ import (
type Addition struct {
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
//DeviceID string `json:"device_id" required:"true"`
RefreshToken string `json:"refresh_token" required:"true"`
DeviceID string `json:"device_id" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
RapidUpload bool `json:"rapid_upload"`

View File

@@ -11,7 +11,7 @@ type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
OauthTokenURL string `json:"oauth_token_url" default:"https://api.alistgo.com/alist/ali_open/token"`
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`

View File

@@ -6,6 +6,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/115_share"
_ "github.com/alist-org/alist/v3/drivers/123"
_ "github.com/alist-org/alist/v3/drivers/123_link"
_ "github.com/alist-org/alist/v3/drivers/123_open"
_ "github.com/alist-org/alist/v3/drivers/123_share"
_ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189"

View File

@@ -11,8 +11,8 @@ type Addition struct {
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
ClientID string `json:"client_id" required:"true" default:"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf"`
ClientSecret string `json:"client_secret" required:"true" default:"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"strings"
"sync"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
@@ -36,88 +37,130 @@ func (d *GithubReleases) Drop(ctx context.Context) error {
return nil
}
// processPoint 处理单个挂载点的文件列表
func (d *GithubReleases) processPoint(point *MountPoint, path string, args model.ListArgs) []File {
var pointFiles []File
if !d.Addition.ShowAllVersion { // latest
point.RequestLatestRelease(d.GetRequest, args.Refresh)
pointFiles = d.processLatestVersion(point, path)
} else { // all version
point.RequestReleases(d.GetRequest, args.Refresh)
pointFiles = d.processAllVersions(point, path)
}
return pointFiles
}
// processLatestVersion 处理最新版本的逻辑
func (d *GithubReleases) processLatestVersion(point *MountPoint, path string) []File {
var pointFiles []File
if point.Point == path { // 与仓库路径相同
pointFiles = append(pointFiles, point.GetLatestRelease()...)
if d.Addition.ShowReadme {
files := point.GetOtherFile(d.GetRequest, false)
pointFiles = append(pointFiles, files...)
}
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
nextDir := GetNextDir(point.Point, path)
if nextDir != "" {
dirFile := File{
Path: path + "/" + nextDir,
FileName: nextDir,
Size: point.GetLatestSize(),
UpdateAt: point.Release.PublishedAt,
CreateAt: point.Release.CreatedAt,
Type: "dir",
Url: "",
}
pointFiles = append(pointFiles, dirFile)
}
}
return pointFiles
}
// processAllVersions 处理所有版本的逻辑
func (d *GithubReleases) processAllVersions(point *MountPoint, path string) []File {
var pointFiles []File
if point.Point == path { // 与仓库路径相同
pointFiles = append(pointFiles, point.GetAllVersion()...)
if d.Addition.ShowReadme {
files := point.GetOtherFile(d.GetRequest, false)
pointFiles = append(pointFiles, files...)
}
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
nextDir := GetNextDir(point.Point, path)
if nextDir != "" {
dirFile := File{
FileName: nextDir,
Path: path + "/" + nextDir,
Size: point.GetAllVersionSize(),
UpdateAt: (*point.Releases)[0].PublishedAt,
CreateAt: (*point.Releases)[0].CreatedAt,
Type: "dir",
Url: "",
}
pointFiles = append(pointFiles, dirFile)
}
} else if strings.HasPrefix(path, point.Point) { // 仓库目录的子目录
tagName := GetNextDir(path, point.Point)
if tagName != "" {
pointFiles = append(pointFiles, point.GetReleaseByTagName(tagName)...)
}
}
return pointFiles
}
// mergeFiles 合并文件列表,处理重复目录
func (d *GithubReleases) mergeFiles(files *[]File, newFiles []File) {
for _, newFile := range newFiles {
if newFile.Type == "dir" {
hasSameDir := false
for index := range *files {
if (*files)[index].GetName() == newFile.GetName() && (*files)[index].Type == "dir" {
hasSameDir = true
(*files)[index].Size += newFile.Size
break
}
}
if !hasSameDir {
*files = append(*files, newFile)
}
} else {
*files = append(*files, newFile)
}
}
}
func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files := make([]File, 0)
path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/"))
for i := range d.points {
point := &d.points[i]
if d.Addition.ConcurrentRequests && d.Addition.Token != "" { // 并发处理
var mu sync.Mutex
var wg sync.WaitGroup
if !d.Addition.ShowAllVersion { // latest
point.RequestRelease(d.GetRequest, args.Refresh)
for i := range d.points {
wg.Add(1)
go func(point *MountPoint) {
defer wg.Done()
pointFiles := d.processPoint(point, path, args)
if point.Point == path { // 与仓库路径相同
files = append(files, point.GetLatestRelease()...)
if d.Addition.ShowReadme {
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
}
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
nextDir := GetNextDir(point.Point, path)
if nextDir == "" {
continue
}
hasSameDir := false
for index := range files {
if files[index].GetName() == nextDir {
hasSameDir = true
files[index].Size += point.GetLatestSize()
break
}
}
if !hasSameDir {
files = append(files, File{
Path: path + "/" + nextDir,
FileName: nextDir,
Size: point.GetLatestSize(),
UpdateAt: point.Release.PublishedAt,
CreateAt: point.Release.CreatedAt,
Type: "dir",
Url: "",
})
}
}
} else { // all version
point.RequestReleases(d.GetRequest, args.Refresh)
if point.Point == path { // 与仓库路径相同
files = append(files, point.GetAllVersion()...)
if d.Addition.ShowReadme {
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...)
}
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录
nextDir := GetNextDir(point.Point, path)
if nextDir == "" {
continue
}
hasSameDir := false
for index := range files {
if files[index].GetName() == nextDir {
hasSameDir = true
files[index].Size += point.GetAllVersionSize()
break
}
}
if !hasSameDir {
files = append(files, File{
FileName: nextDir,
Path: path + "/" + nextDir,
Size: point.GetAllVersionSize(),
UpdateAt: (*point.Releases)[0].PublishedAt,
CreateAt: (*point.Releases)[0].CreatedAt,
Type: "dir",
Url: "",
})
}
} else if strings.HasPrefix(path, point.Point) { // 仓库目录的子目录
tagName := GetNextDir(path, point.Point)
if tagName == "" {
continue
}
files = append(files, point.GetReleaseByTagName(tagName)...)
}
mu.Lock()
d.mergeFiles(&files, pointFiles)
mu.Unlock()
}(&d.points[i])
}
wg.Wait()
} else { // 串行处理
for i := range d.points {
point := &d.points[i]
pointFiles := d.processPoint(point, path, args)
d.mergeFiles(&files, pointFiles)
}
}

View File

@@ -7,11 +7,12 @@ import (
type Addition struct {
driver.RootID
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"alistGo/alist" help:"structure:[path:]org/repo"`
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
GitHubProxy string `json:"gh_proxy" type:"string" default:"" help:"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com "`
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"alistGo/alist" help:"structure:[path:]org/repo"`
ShowReadme bool `json:"show_readme" type:"bool" default:"true" help:"show README、LICENSE file"`
Token string `json:"token" type:"string" required:"false" help:"GitHub token, if you want to access private repositories or increase the rate limit"`
ShowAllVersion bool `json:"show_all_version" type:"bool" default:"false" help:"show all versions"`
ConcurrentRequests bool `json:"concurrent_requests" type:"bool" default:"false" help:"To concurrently request the GitHub API, you must enter a GitHub token"`
GitHubProxy string `json:"gh_proxy" type:"string" default:"" help:"GitHub proxy, e.g. https://ghproxy.net/github.com or https://gh-proxy.com/github.com "`
}
var config = driver.Config{

View File

@@ -18,7 +18,7 @@ type MountPoint struct {
}
// 请求最新版本
func (m *MountPoint) RequestRelease(get func(url string) (*resty.Response, error), refresh bool) {
func (m *MountPoint) RequestLatestRelease(get func(url string) (*resty.Response, error), refresh bool) {
if m.Repo == "" {
return
}

View File

@@ -6,8 +6,8 @@ import (
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
// 发送 GET 请求
@@ -23,7 +23,7 @@ func (d *GithubReleases) GetRequest(url string) (*resty.Response, error) {
return nil, err
}
if res.StatusCode() != 200 {
log.Warn("failed to get request: ", res.StatusCode(), res.String())
utils.Log.Warnf("failed to get request: %s %d %s", url, res.StatusCode(), res.String())
}
return res, nil
}

View File

@@ -83,7 +83,7 @@ type Group struct {
Type int `json:"type"`
Name string `json:"name"`
IsAdministrator int `json:"is_administrator"`
Role int `json:"role"`
Role []int `json:"role"`
Avatar string `json:"avatar_url"`
IsStick int `json:"is_stick"`
Nickname string `json:"nickname"`

View File

@@ -3,6 +3,7 @@ package data
import "github.com/alist-org/alist/v3/cmd/flags"
func InitData() {
initRoles()
initUser()
initSettings()
initTasks()

View File

@@ -26,7 +26,7 @@ func initDevData() {
Username: "Noah",
Password: "hsu",
BasePath: "/data",
Role: 0,
Role: nil,
Permission: 512,
})
if err != nil {

View File

@@ -0,0 +1,52 @@
package data
// initRoles creates the default admin and guest roles if missing.
// These roles are essential and must not be modified or removed.
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func initRoles() {
guestRole, err := op.GetRoleByName("guest")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
guestRole = &model.Role{
ID: uint(model.GUEST),
Name: "guest",
Description: "Guest",
PermissionScopes: []model.PermissionEntry{
{Path: "/", Permission: 0},
},
}
if err := op.CreateRole(guestRole); err != nil {
utils.Log.Fatalf("[init role] Failed to create guest role: %v", err)
}
} else {
utils.Log.Fatalf("[init role] Failed to get guest role: %v", err)
}
}
_, err = op.GetRoleByName("admin")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
adminRole := &model.Role{
ID: uint(model.ADMIN),
Name: "admin",
Description: "Administrator",
PermissionScopes: []model.PermissionEntry{
{Path: "/", Permission: 0xFFFF},
},
}
if err := op.CreateRole(adminRole); err != nil {
utils.Log.Fatalf("[init role] Failed to create admin role: %v", err)
}
} else {
utils.Log.Fatalf("[init role] Failed to get admin role: %v", err)
}
}
}

View File

@@ -103,6 +103,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},
// newui settings
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
// style settings
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},

View File

@@ -1,10 +1,10 @@
package data
import (
"github.com/alist-org/alist/v3/internal/db"
"os"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
@@ -14,45 +14,16 @@ import (
)
func initUser() {
admin, err := op.GetAdmin()
adminPassword := random.String(8)
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
if flags.Dev {
adminPassword = "admin"
} else if len(envpass) > 0 {
adminPassword = envpass
}
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16)
admin = &model.User{
Username: "admin",
Salt: salt,
PwdHash: model.TwoHashPwd(adminPassword, salt),
Role: model.ADMIN,
BasePath: "/",
Authn: "[]",
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
Permission: 0x30FF,
}
if err := op.CreateUser(admin); err != nil {
panic(err)
} else {
utils.Log.Infof("Successfully created the admin user and the initial password is: %s", adminPassword)
}
} else {
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
}
}
guest, err := op.GetGuest()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16)
guestRole, _ := op.GetRoleByName("guest")
guest = &model.User{
Username: "guest",
PwdHash: model.TwoHashPwd("guest", salt),
Salt: salt,
Role: model.GUEST,
Role: model.Roles{int(guestRole.ID)},
BasePath: "/",
Permission: 0,
Disabled: true,
@@ -65,4 +36,35 @@ func initUser() {
utils.Log.Fatalf("[init user] Failed to get guest user: %v", err)
}
}
admin, err := op.GetAdmin()
adminPassword := random.String(8)
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
if flags.Dev {
adminPassword = "admin"
} else if len(envpass) > 0 {
adminPassword = envpass
}
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16)
adminRole, _ := op.GetRoleByName("admin")
admin = &model.User{
Username: "admin",
Salt: salt,
PwdHash: model.TwoHashPwd(adminPassword, salt),
Role: model.Roles{int(adminRole.ID)},
BasePath: "/",
Authn: "[]",
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
Permission: 0xFFFF,
}
if err := op.CreateUser(admin); err != nil {
panic(err)
} else {
utils.Log.Infof("Successfully created the admin user and the initial password is: %s", adminPassword)
}
} else {
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
}
}
}

View File

@@ -14,10 +14,14 @@ import (
func init() {
formatter := logrus.TextFormatter{
ForceColors: true,
EnvironmentOverrideColors: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
}
if os.Getenv("NO_COLOR") != "" || os.Getenv("ALIST_NO_COLOR") == "1" {
formatter.DisableColors = true
} else {
formatter.ForceColors = true
formatter.EnvironmentOverrideColors = true
}
logrus.SetFormatter(&formatter)
utils.Log.SetFormatter(&formatter)

View File

@@ -4,6 +4,7 @@ import (
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_24_0"
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_32_0"
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_41_0"
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
)
type VersionPatches struct {
@@ -32,4 +33,10 @@ var UpgradePatches = []VersionPatches{
v3_41_0.GrantAdminPermissions,
},
},
{
Version: "v3.46.0",
Patches: []func(){
v3_46_0.ConvertLegacyRoles,
},
},
}

View File

@@ -0,0 +1,186 @@
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"
"github.com/alist-org/alist/v3/pkg/utils"
"gorm.io/gorm"
)
// ConvertLegacyRoles migrates old integer role values to a new role model with permission scopes.
func ConvertLegacyRoles() {
guestRole, err := op.GetRoleByName("guest")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
guestRole = &model.Role{
ID: uint(model.GUEST),
Name: "guest",
Description: "Guest",
PermissionScopes: []model.PermissionEntry{
{
Path: "/",
Permission: 0,
},
},
}
if err = op.CreateRole(guestRole); err != nil {
utils.Log.Errorf("[convert roles] failed to create guest role: %v", err)
return
}
} else {
utils.Log.Errorf("[convert roles] failed to get guest role: %v", err)
return
}
}
adminRole, err := op.GetRoleByName("admin")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
adminRole = &model.Role{
ID: uint(model.ADMIN),
Name: "admin",
Description: "Administrator",
PermissionScopes: []model.PermissionEntry{
{
Path: "/",
Permission: 0x33FF,
},
},
}
if err = op.CreateRole(adminRole); err != nil {
utils.Log.Errorf("[convert roles] failed to create admin role: %v", err)
return
}
} else {
utils.Log.Errorf("[convert roles] failed to get admin role: %v", err)
return
}
}
generalRole, err := op.GetRoleByName("general")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
generalRole = &model.Role{
ID: uint(model.NEWGENERAL),
Name: "general",
Description: "General User",
PermissionScopes: []model.PermissionEntry{
{
Path: "/",
Permission: 0,
},
},
}
if err = op.CreateRole(generalRole); err != nil {
utils.Log.Errorf("[convert roles] failed create general role: %v", err)
return
}
} else {
utils.Log.Errorf("[convert roles] failed get general role: %v", err)
return
}
}
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()
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
}
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:
newRoles = append(newRoles, int(adminRole.ID))
case model.GUEST:
newRoles = append(newRoles, int(guestRole.ID))
case model.GENERAL:
newRoles = append(newRoles, int(generalRole.ID))
default:
newRoles = append(newRoles, r)
}
}
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", 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
}

View File

@@ -16,6 +16,7 @@ const (
AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
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))
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())
}

79
internal/db/label.go Normal file
View File

@@ -0,0 +1,79 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"gorm.io/gorm"
"time"
)
// GetLabels Get all label from database order by id
func GetLabels(pageIndex, pageSize int) ([]model.Label, int64, error) {
labelDB := db.Model(&model.Label{})
var count int64
if err := labelDB.Count(&count).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get label count")
}
var labels []model.Label
if err := labelDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&labels).Error; err != nil {
return nil, 0, errors.WithStack(err)
}
return labels, count, nil
}
// GetLabelById Get Label by id, used to update label usually
func GetLabelById(id uint) (*model.Label, error) {
var label model.Label
label.ID = id
if err := db.First(&label).Error; err != nil {
return nil, errors.WithStack(err)
}
return &label, nil
}
// CreateLabel just insert label to database
func CreateLabel(label model.Label) (uint, error) {
label.CreateTime = time.Now()
err := errors.WithStack(db.Create(&label).Error)
if err != nil {
return label.ID, errors.WithMessage(err, "failed create label in database")
}
return label.ID, nil
}
// UpdateLabel just update storage in database
func UpdateLabel(label *model.Label) (*model.Label, error) {
label.CreateTime = time.Now()
_, err := GetLabelById(label.ID)
if err != nil {
return nil, errors.WithMessage(err, "failed get old label")
}
err = errors.WithStack(db.Save(label).Error)
if err != nil {
return nil, errors.WithMessage(err, "failed create label in database")
}
return label, nil
}
// DeleteLabelById just delete label from database by id
func DeleteLabelById(id uint) error {
return errors.WithStack(db.Delete(&model.Label{}, id).Error)
}
// GetLabelByIds Get label from database order by ids
func GetLabelByIds(ids []uint) ([]model.Label, error) {
labelDB := db.Model(&model.Label{})
var labels []model.Label
if err := labelDB.Where(ids).Find(&labels).Error; err != nil {
return nil, errors.WithStack(err)
}
return labels, nil
}
// GetLabelByName Get Label by name
func GetLabelByName(name string) bool {
var label model.Label
result := db.Where("name = ?", name).First(&label)
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
return exists
}

View File

@@ -0,0 +1,192 @@
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) {
//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)
}
return labelIds, nil
}
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
var labelFileBinDing model.LabelFileBinding
labelFileBinDing.UserId = userId
labelFileBinDing.LabelId = labelId
labelFileBinDing.FileName = fileName
labelFileBinDing.CreateTime = time.Now()
err := errors.WithStack(db.Create(&labelFileBinDing).Error)
if err != nil {
return errors.WithMessage(err, "failed create label in database")
}
return nil
}
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
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
}
// 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)
}
// 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)
}
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)
}

31
internal/db/obj_file.go Normal file
View File

@@ -0,0 +1,31 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"gorm.io/gorm"
)
// GetFileByNameExists Get file by name
func GetFileByNameExists(name string) bool {
var label model.ObjFile
result := db.Where("name = ?", name).First(&label)
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
return exists
}
// GetFileByName Get file by name
func GetFileByName(name string, userId uint) (objFile model.ObjFile, err error) {
if err = db.Where("name = ?", name).Where("user_id = ?", userId).First(&objFile).Error; err != nil {
return objFile, errors.WithStack(err)
}
return objFile, nil
}
func CreateObjFile(obj model.ObjFile) error {
err := errors.WithStack(db.Create(&obj).Error)
if err != nil {
return errors.WithMessage(err, "failed create file in database")
}
return nil
}

79
internal/db/role.go Normal file
View File

@@ -0,0 +1,79 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"path"
"strings"
)
func GetRole(id uint) (*model.Role, error) {
var r model.Role
if err := db.First(&r, id).Error; err != nil {
return nil, errors.Wrapf(err, "failed get role")
}
return &r, nil
}
func GetRoleByName(name string) (*model.Role, error) {
r := model.Role{Name: name}
if err := db.Where(r).First(&r).Error; err != nil {
return nil, errors.Wrapf(err, "failed get role")
}
return &r, nil
}
func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err error) {
roleDB := db.Model(&model.Role{})
if err = roleDB.Count(&count).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get roles count")
}
if err = roleDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&roles).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get find roles")
}
return roles, count, nil
}
func CreateRole(r *model.Role) error {
return errors.WithStack(db.Create(r).Error)
}
func UpdateRole(r *model.Role) error {
return errors.WithStack(db.Save(r).Error)
}
func DeleteRole(id uint) error {
return errors.WithStack(db.Delete(&model.Role{}, id).Error)
}
func UpdateRolePermissionsPathPrefix(oldPath, newPath string) ([]uint, error) {
var roles []model.Role
var modifiedRoleIDs []uint
if err := db.Find(&roles).Error; err != nil {
return nil, errors.WithMessage(err, "failed to load roles")
}
for _, role := range roles {
updated := false
for i, entry := range role.PermissionScopes {
entryPath := path.Clean(entry.Path)
oldPathClean := path.Clean(oldPath)
if entryPath == oldPathClean {
role.PermissionScopes[i].Path = newPath
updated = true
} else if strings.HasPrefix(entryPath, oldPathClean+"/") {
role.PermissionScopes[i].Path = newPath + entryPath[len(oldPathClean):]
updated = true
}
}
if updated {
if err := UpdateRole(&role); err != nil {
return nil, errors.WithMessagef(err, "failed to update role ID %d", role.ID)
}
modifiedRoleIDs = append(modifiedRoleIDs, role.ID)
}
}
return modifiedRoleIDs, nil
}

View File

@@ -2,19 +2,42 @@ package db
import (
"encoding/base64"
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
"gorm.io/gorm"
"path"
"slices"
"strings"
)
func GetUserByRole(role int) (*model.User, error) {
user := model.User{Role: role}
if err := db.Where(user).Take(&user).Error; err != nil {
var users []model.User
if err := db.Find(&users).Error; err != nil {
return nil, err
}
return &user, nil
for i := range users {
if users[i].Role.Contains(role) {
return &users[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
func GetUsersByRole(roleID int) ([]model.User, error) {
var users []model.User
if err := db.Find(&users).Error; err != nil {
return nil, err
}
var result []model.User
for _, u := range users {
if slices.Contains(u.Role, roleID) {
result = append(result, u)
}
}
return result, nil
}
func GetUserByName(username string) (*model.User, error) {
@@ -100,3 +123,50 @@ func RemoveAuthn(u *model.User, id string) error {
}
return UpdateAuthn(u.ID, string(res))
}
func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {
var users []model.User
var modifiedUsernames []string
oldPathClean := path.Clean(oldPath)
if len(usersOpt) > 0 {
users = usersOpt[0]
} else {
if err := db.Find(&users).Error; err != nil {
return nil, errors.WithMessage(err, "failed to load users")
}
}
for _, user := range users {
basePath := path.Clean(user.BasePath)
updated := false
if basePath == oldPathClean {
user.BasePath = path.Clean(newPath)
updated = true
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
user.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])
updated = true
}
if updated {
if err := UpdateUser(&user); err != nil {
return nil, errors.WithMessagef(err, "failed to update user ID %d", user.ID)
}
modifiedUsernames = append(modifiedUsernames, user.Username)
}
}
return modifiedUsernames, nil
}
func CountUsersByRoleAndEnabledExclude(roleID uint, excludeUserID uint) (int64, error) {
var count int64
jsonValue := fmt.Sprintf("[%d]", roleID)
err := db.Model(&model.User{}).
Where("disabled = ? AND id != ?", false, excludeUserID).
Where("JSON_CONTAINS(role, ?)", jsonValue).
Count(&count).Error
return count, err
}

View File

@@ -4,4 +4,5 @@ import "errors"
var (
EmptyToken = errors.New("empty token")
LinkIsDir = errors.New("link is dir")
)

7
internal/errs/role.go Normal file
View File

@@ -0,0 +1,7 @@
package errs
import "errors"
var (
ErrChangeDefaultRole = errors.New("cannot modify admin role")
)

View File

@@ -6,6 +6,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -45,8 +46,13 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
}
func whetherHide(user *model.User, meta *model.Meta, path string) bool {
// if is admin, don't hide
if user == nil || user.CanSeeHides() {
// if user is nil, don't hide
if user == nil {
return false
}
perm := common.MergeRolePermissions(user, path)
// if user has see-hides permission, don't hide
if common.HasPermission(perm, common.PermSeeHides) {
return false
}
// if meta is nil, don't hide

12
internal/model/label.go Normal file
View File

@@ -0,0 +1,12 @@
package model
import "time"
type Label struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Type int `json:"type"` // use to type
Name string `json:"name"` // use to name
Description string `json:"description"` // use to description
BgColor string `json:"bg_color"` // use to bg_color
CreateTime time.Time `json:"create_time"`
}

View File

@@ -0,0 +1,11 @@
package model
import "time"
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
FileName string `json:"file_name"` // use to file_name
CreateTime time.Time `json:"create_time"`
}

View File

@@ -55,6 +55,21 @@ type FileStreamer interface {
type UpdateProgress func(percentage float64)
// Reference implementation from OpenListTeam:
// https://github.com/OpenListTeam/OpenList/blob/a703b736c9346c483bae56905a39bc07bf781cff/internal/model/obj.go#L58
func UpdateProgressWithRange(inner UpdateProgress, start, end float64) UpdateProgress {
return func(p float64) {
if p < 0 {
p = 0
}
if p > 100 {
p = 100
}
scaled := start + (end-start)*(p/100.0)
inner(scaled)
}
}
type URL interface {
URL() string
}

View File

@@ -0,0 +1,18 @@
package model
import "time"
type ObjFile struct {
Id string `json:"id"`
UserId uint `json:"user_id"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
HashInfoStr string `json:"hashinfo"`
}

27
internal/model/paths.go Normal file
View File

@@ -0,0 +1,27 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
type Paths []string
func (p Paths) Value() (driver.Value, error) {
return json.Marshal([]string(p))
}
func (p *Paths) Scan(value interface{}) error {
switch v := value.(type) {
case []byte:
return json.Unmarshal(v, (*[]string)(p))
case string:
return json.Unmarshal([]byte(v), (*[]string)(p))
case nil:
*p = nil
return nil
default:
return fmt.Errorf("cannot scan %T", value)
}
}

52
internal/model/role.go Normal file
View File

@@ -0,0 +1,52 @@
package model
import (
"encoding/json"
"gorm.io/gorm"
)
// PermissionEntry defines permission bitmask for a specific path.
type PermissionEntry struct {
Path string `json:"path"` // path prefix, e.g. "/admin"
Permission int32 `json:"permission"` // bitmask permissions
}
// Role represents a permission template which can be bound to users.
type Role struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique" binding:"required"`
Description string `json:"description"`
// 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.
RawPermission string `json:"-" gorm:"type:text"`
}
// BeforeSave GORM hook serializes PermissionScopes into RawPermission.
func (r *Role) BeforeSave(tx *gorm.DB) error {
if len(r.PermissionScopes) == 0 {
r.RawPermission = ""
return nil
}
bs, err := json.Marshal(r.PermissionScopes)
if err != nil {
return err
}
r.RawPermission = string(bs)
return nil
}
// AfterFind GORM hook deserializes RawPermission into PermissionScopes.
func (r *Role) AfterFind(tx *gorm.DB) error {
if r.RawPermission == "" {
r.PermissionScopes = nil
return nil
}
var scopes []PermissionEntry
if err := json.Unmarshal([]byte(r.RawPermission), &scopes); err != nil {
return err
}
r.PermissionScopes = scopes
return nil
}

36
internal/model/roles.go Normal file
View File

@@ -0,0 +1,36 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
type Roles []int
func (r Roles) Value() (driver.Value, error) {
return json.Marshal([]int(r))
}
func (r *Roles) Scan(value interface{}) error {
switch v := value.(type) {
case []byte:
return json.Unmarshal(v, (*[]int)(r))
case string:
return json.Unmarshal([]byte(v), (*[]int)(r))
case nil:
*r = nil
return nil
default:
return fmt.Errorf("cannot scan %T", value)
}
}
func (r Roles) Contains(role int) bool {
for _, v := range r {
if v == role {
return true
}
}
return false
}

View File

@@ -17,20 +17,22 @@ const (
GENERAL = iota
GUEST // only one exists
ADMIN
NEWGENERAL
)
const StaticHashSalt = "https://github.com/alist-org/alist"
type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username
PwdHash string `json:"-"` // password hash
PwdTS int64 `json:"-"` // password timestamp
Salt string `json:"-"` // unique salt
Password string `json:"password"` // password
BasePath string `json:"base_path"` // base path
Role int `json:"role"` // user's role
Disabled bool `json:"disabled"`
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username
PwdHash string `json:"-"` // password hash
PwdTS int64 `json:"-"` // password timestamp
Salt string `json:"-"` // unique salt
Password string `json:"password"` // password
BasePath string `json:"base_path"` // base path
Role Roles `json:"role" gorm:"type:text"` // user's roles
RolesDetail []Role `json:"-" gorm:"-"`
Disabled bool `json:"disabled"`
// Determine permissions by bit
// 0: can see hidden files
// 1: can access without password
@@ -46,6 +48,7 @@ type User struct {
// 11: ftp/sftp write
// 12: can read archives
// 13: can decompress archives
// 14: check path limit
Permission int32 `json:"permission"`
OtpSecret string `json:"-"`
SsoID string `json:"sso_id"` // unique by sso platform
@@ -53,11 +56,11 @@ type User struct {
}
func (u *User) IsGuest() bool {
return u.Role == GUEST
return u.Role.Contains(GUEST)
}
func (u *User) IsAdmin() bool {
return u.Role == ADMIN
return u.Role.Contains(ADMIN)
}
func (u *User) ValidateRawPassword(password string) error {
@@ -137,8 +140,34 @@ func (u *User) CanDecompress() bool {
return (u.Permission>>13)&1 == 1
}
func (u *User) CheckPathLimit() bool {
return (u.Permission>>14)&1 == 1
}
func (u *User) JoinPath(reqPath string) (string, error) {
return utils.JoinBasePath(u.BasePath, reqPath)
if reqPath == "/" {
return utils.FixAndCleanPath(u.BasePath), nil
}
path, err := utils.JoinBasePath(u.BasePath, reqPath)
if err != nil {
return "", err
}
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
}
func StaticHash(password string) string {
@@ -179,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
}

24
internal/op/label.go Normal file
View File

@@ -0,0 +1,24 @@
package op
import (
"context"
"github.com/alist-org/alist/v3/internal/db"
"github.com/pkg/errors"
)
func DeleteLabelById(ctx context.Context, id, userId uint) error {
_, err := db.GetLabelById(id)
if err != nil {
return errors.WithMessage(err, "failed get label")
}
if db.GetLabelFileBinDingByLabelIdExists(id, userId) {
return errors.New("label have binding relationships")
}
// delete the label in the database
if err := db.DeleteLabelById(id); err != nil {
return errors.WithMessage(err, "failed delete label in database")
}
return nil
}

View File

@@ -0,0 +1,195 @@
package op
import (
"fmt"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"strconv"
"strings"
"time"
)
type CreateLabelFileBinDingReq struct {
Id string `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
HashInfoStr string `json:"hashinfo"`
LabelIds string `json:"label_ids"`
LabelIDs []uint64 `json:"labelIdList"`
}
type ObjLabelResp struct {
Id string `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
HashInfoStr string `json:"hashinfo"`
LabelList []model.Label `json:"label_list"`
}
func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
labelIds, err := db.GetLabelIds(userId, fileName)
if err != nil {
return nil, errors.WithMessage(err, "failed get label_file_binding")
}
var labels []model.Label
if len(labelIds) > 0 {
if labels, err = db.GetLabelByIds(labelIds); err != nil {
return nil, errors.WithMessage(err, "failed labels in database")
}
}
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")
}
ids, err := collectLabelIDs(req)
if err != nil {
return err
}
if len(ids) == 0 {
return 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,
UserId: userId,
Path: req.Path,
Name: req.Name,
Size: req.Size,
IsDir: req.IsDir,
Modified: req.Modified,
Created: req.Created,
Sign: req.Sign,
Thumb: req.Thumb,
Type: req.Type,
HashInfoStr: req.HashInfoStr,
}
if err := db.CreateObjFile(objFile); err != nil {
return errors.WithMessage(err, "failed file in database")
}
}
return nil
}
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
labelMap := strings.Split(labelId, ",")
var labelIds []uint
var labelsFile []model.LabelFileBinding
var labels []model.Label
var labelsFileMap = make(map[string][]model.Label)
var labelsMap = make(map[uint]model.Label)
if labelIds, err = StringSliceToUintSlice(labelMap); err != nil {
return nil, errors.WithMessage(err, "failed string to uint err")
}
//查询标签信息
if labels, err = db.GetLabelByIds(labelIds); err != nil {
return nil, errors.WithMessage(err, "failed labels in database")
}
for _, val := range labels {
labelsMap[val.ID] = val
}
//查询标签对应文件名列表
if labelsFile, err = db.GetLabelFileBinDingByLabelId(labelIds, userId); err != nil {
return nil, errors.WithMessage(err, "failed labels in database")
}
for _, value := range labelsFile {
var labelTemp model.Label
labelTemp = labelsMap[value.LabelId]
labelsFileMap[value.FileName] = append(labelsFileMap[value.FileName], labelTemp)
}
for index, v := range labelsFileMap {
objFile, err := db.GetFileByName(index, userId)
if err != nil {
return nil, errors.WithMessage(err, "failed GetFileByName in database")
}
objLabel := ObjLabelResp{
Id: objFile.Id,
Path: objFile.Path,
Name: objFile.Name,
Size: objFile.Size,
IsDir: objFile.IsDir,
Modified: objFile.Modified,
Created: objFile.Created,
Sign: objFile.Sign,
Thumb: objFile.Thumb,
Type: objFile.Type,
HashInfoStr: objFile.HashInfoStr,
LabelList: v,
}
result = append(result, objLabel)
}
return result, nil
}
func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
uintSlice := make([]uint, len(strSlice))
for i, str := range strSlice {
// 使用strconv.ParseUint将字符串转换为uint64
uint64Value, err := strconv.ParseUint(str, 10, 64)
if err != nil {
return nil, err // 如果转换失败,返回错误
}
// 将uint64值转换为uint注意这里可能存在精度损失如果uint64值超出了uint的范围
uintSlice[i] = uint(uint64Value)
}
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
}

148
internal/op/role.go Normal file
View File

@@ -0,0 +1,148 @@
package op
import (
"fmt"
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/singleflight"
"github.com/alist-org/alist/v3/pkg/utils"
)
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 {
return r, nil
}
r, err, _ := roleG.Do(key, func() (*model.Role, error) {
_r, err := db.GetRole(id)
if err != nil {
return nil, err
}
roleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))
return _r, nil
})
return r, err
}
func GetRoleByName(name string) (*model.Role, error) {
if r, ok := roleCache.Get(name); ok {
return r, nil
}
r, err, _ := roleG.Do(name, func() (*model.Role, error) {
_r, err := db.GetRoleByName(name)
if err != nil {
return nil, err
}
roleCache.Set(name, _r, cache.WithEx[*model.Role](time.Hour))
return _r, nil
})
return r, err
}
func GetRolesByUserID(userID uint) ([]model.Role, error) {
user, err := GetUserById(userID)
if err != nil {
return nil, err
}
var roles []model.Role
for _, roleID := range user.Role {
key := fmt.Sprint(roleID)
if r, ok := roleCache.Get(key); ok {
roles = append(roles, *r)
continue
}
r, err, _ := roleG.Do(key, func() (*model.Role, error) {
_r, err := db.GetRole(uint(roleID))
if err != nil {
return nil, err
}
roleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))
return _r, nil
})
if err != nil {
return nil, err
}
roles = append(roles, *r)
}
return roles, nil
}
func GetRoles(pageIndex, pageSize int) ([]model.Role, int64, error) {
return db.GetRoles(pageIndex, pageSize)
}
func CreateRole(r *model.Role) error {
for i := range r.PermissionScopes {
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.CreateRole(r)
}
func UpdateRole(r *model.Role) error {
old, err := db.GetRole(r.ID)
if err != nil {
return err
}
switch old.Name {
case "admin":
return errs.ErrChangeDefaultRole
case "guest":
r.Name = "guest"
}
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)
// }
//}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.UpdateRole(r)
}
func DeleteRole(id uint) error {
old, err := db.GetRole(id)
if err != nil {
return err
}
if old.Name == "admin" || old.Name == "guest" {
return errs.ErrChangeDefaultRole
}
roleCache.Del(fmt.Sprint(id))
roleCache.Del(old.Name)
return db.DeleteRole(id)
}

View File

@@ -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")
@@ -216,6 +224,29 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
if oldStorage.MountPath != storage.MountPath {
// mount path renamed, need to drop the storage
storagesMap.Delete(oldStorage.MountPath)
modifiedRoleIDs, err := db.UpdateRolePermissionsPathPrefix(oldStorage.MountPath, storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed to update role permissions")
}
for _, id := range modifiedRoleIDs {
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 _, 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 {
return errors.WithMessage(err, "failed get storage driver")

View File

@@ -18,7 +18,11 @@ var adminUser *model.User
func GetAdmin() (*model.User, error) {
if adminUser == nil {
user, err := db.GetUserByRole(model.ADMIN)
role, err := GetRoleByName("admin")
if err != nil {
return nil, err
}
user, err := db.GetUserByRole(int(role.ID))
if err != nil {
return nil, err
}
@@ -29,7 +33,11 @@ func GetAdmin() (*model.User, error) {
func GetGuest() (*model.User, error) {
if guestUser == nil {
user, err := db.GetUserByRole(model.GUEST)
role, err := GetRoleByName("guest")
if err != nil {
return nil, err
}
user, err := db.GetUserByRole(int(role.ID))
if err != nil {
return nil, err
}
@@ -42,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
@@ -70,7 +82,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 {
@@ -98,6 +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
// }
// }
// }
//}
return db.UpdateUser(u)
}
@@ -128,3 +169,11 @@ func DelUserCache(username string) error {
userCache.Del(username)
return nil
}
func CountEnabledAdminsExcluding(userID uint) (int64, error) {
adminRole, err := GetRoleByName("admin")
if err != nil {
return 0, err
}
return db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID)
}

View File

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

View File

@@ -1,15 +1,11 @@
package common
import (
"path"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/dlclark/regexp2"
)
func IsStorageSignEnabled(rawPath string) bool {
@@ -32,30 +28,11 @@ func IsApply(metaPath, reqPath string, applySub bool) bool {
}
func CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {
// if the reqPath is in hide (only can check the nearest meta) and user can't see hides, can't access
if meta != nil && !user.CanSeeHides() && meta.Hide != "" &&
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path
for _, hide := range strings.Split(meta.Hide, "\n") {
re := regexp2.MustCompile(hide, regexp2.None)
if isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {
return false
}
}
}
// if is not guest and can access without password
if user.CanAccessWithoutPassword() {
return true
}
// if meta is nil or password is empty, can access
if meta == nil || meta.Password == "" {
return true
}
// if meta doesn't apply to sub_folder, can access
if !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {
return true
}
// validate password
return meta.Password == password
// Deprecated: CanAccess is kept for backward compatibility.
// The logic has been moved to CanAccessWithRoles which performs the
// necessary checks based on role permissions. This wrapper ensures
// older calls still work without relying on user permission bits.
return CanAccessWithRoles(user, meta, reqPath, password)
}
// ShouldProxy TODO need optimize

137
server/common/role_perm.go Normal file
View File

@@ -0,0 +1,137 @@
package common
import (
"path"
"strings"
"github.com/dlclark/regexp2"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
const (
PermSeeHides = iota
PermAccessWithoutPassword
PermAddOfflineDownload
PermWrite
PermRename
PermMove
PermCopy
PermRemove
PermWebdavRead
PermWebdavManage
PermFTPAccess
PermFTPManage
PermReadArchives
PermDecompress
PermPathLimit
)
func HasPermission(perm int32, bit uint) bool {
return (perm>>bit)&1 == 1
}
func MergeRolePermissions(u *model.User, reqPath string) int32 {
if u == nil {
return 0
}
var perm int32
for _, rid := range u.Role {
role, err := op.GetRole(uint(rid))
if err != nil {
continue
}
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) {
return false
}
perm := MergeRolePermissions(u, reqPath)
if meta != nil && !HasPermission(perm, PermSeeHides) && meta.Hide != "" &&
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) {
for _, hide := range strings.Split(meta.Hide, "\n") {
re := regexp2.MustCompile(hide, regexp2.None)
if isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {
return false
}
}
}
if HasPermission(perm, PermAccessWithoutPassword) {
return true
}
if meta == nil || meta.Password == "" {
return true
}
if !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {
return true
}
return meta.Password == password
}
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
}
for _, rid := range u.Role {
role, err := op.GetRole(uint(rid))
if err != nil {
continue
}
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
return true
}
}
}
return false
}
// CheckPathLimitWithRoles checks whether the path is allowed when the user has
// the `PermPathLimit` permission for the target path. When the user does not
// have this permission, the check passes by default.
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
perm := MergeRolePermissions(u, reqPath)
if HasPermission(perm, PermPathLimit) {
return CanReadPathByRole(u, reqPath)
}
return true
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/ftp"
"math/rand"
"net"
@@ -130,7 +131,8 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
return nil, err
}
}
if userObj.Disabled || !userObj.CanFTPAccess() {
perm := common.MergeRolePermissions(userObj, userObj.BasePath)
if userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {
return nil, errors.New("user is not allowed to access via FTP")
}

View File

@@ -18,7 +18,8 @@ func Mkdir(ctx context.Context, path string) error {
if err != nil {
return err
}
if !user.CanWrite() || !user.CanFTPManage() {
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermWrite) || !common.HasPermission(perm, common.PermFTPManage) {
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
@@ -34,7 +35,8 @@ func Mkdir(ctx context.Context, path string) error {
func Remove(ctx context.Context, path string) error {
user := ctx.Value("user").(*model.User)
if !user.CanRemove() || !user.CanFTPManage() {
perm := common.MergeRolePermissions(user, path)
if !common.HasPermission(perm, common.PermRemove) || !common.HasPermission(perm, common.PermFTPManage) {
return errs.PermissionDenied
}
reqPath, err := user.JoinPath(path)
@@ -56,13 +58,14 @@ func Rename(ctx context.Context, oldPath, newPath string) error {
}
srcDir, srcBase := stdpath.Split(srcPath)
dstDir, dstBase := stdpath.Split(dstPath)
permSrc := common.MergeRolePermissions(user, srcPath)
if srcDir == dstDir {
if !user.CanRename() || !user.CanFTPManage() {
if !common.HasPermission(permSrc, common.PermRename) || !common.HasPermission(permSrc, common.PermFTPManage) {
return errs.PermissionDenied
}
return fs.Rename(ctx, srcPath, dstBase)
} else {
if !user.CanFTPManage() || !user.CanMove() || (srcBase != dstBase && !user.CanRename()) {
if !common.HasPermission(permSrc, common.PermFTPManage) || !common.HasPermission(permSrc, common.PermMove) || (srcBase != dstBase && !common.HasPermission(permSrc, common.PermRename)) {
return errs.PermissionDenied
}
if err = fs.Move(ctx, srcPath, dstDir); err != nil {

View File

@@ -30,7 +30,7 @@ func OpenDownload(ctx context.Context, reqPath string, offset int64) (*FileDownl
}
}
ctx = context.WithValue(ctx, "meta", meta)
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
return nil, errs.PermissionDenied
}
@@ -125,7 +125,7 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) {
}
}
ctx = context.WithValue(ctx, "meta", meta)
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
return nil, errs.PermissionDenied
}
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
@@ -148,7 +148,7 @@ func List(ctx context.Context, path string) ([]os.FileInfo, error) {
}
}
ctx = context.WithValue(ctx, "meta", meta)
if !common.CanAccess(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
if !common.CanAccessWithRoles(user, meta, reqPath, ctx.Value("meta_pass").(string)) {
return nil, errs.PermissionDenied
}
objs, err := fs.List(ctx, reqPath, &fs.ListArgs{})

View File

@@ -35,8 +35,10 @@ func uploadAuth(ctx context.Context, path string) error {
return err
}
}
if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) &&
((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
perm := common.MergeRolePermissions(user, path)
if !(common.CanAccessWithRoles(user, meta, path, ctx.Value("meta_pass").(string)) &&
((common.HasPermission(perm, common.PermFTPManage) && common.HasPermission(perm, common.PermWrite)) ||
common.CanWrite(meta, stdpath.Dir(path)))) {
return errs.PermissionDenied
}
return nil

View File

@@ -78,15 +78,20 @@ func FsArchiveMeta(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanReadArchives() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqPath, err := user.JoinPath(req.Path)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermReadArchives) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
meta, err := op.GetNearestMeta(reqPath)
if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
@@ -156,15 +161,20 @@ func FsArchiveList(c *gin.Context) {
}
req.Validate()
user := c.MustGet("user").(*model.User)
if !user.CanReadArchives() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqPath, err := user.JoinPath(req.Path)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermReadArchives) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
meta, err := op.GetNearestMeta(reqPath)
if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
@@ -242,10 +252,6 @@ func FsArchiveDecompress(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanDecompress() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcPaths := make([]string, 0, len(req.Name))
for _, name := range req.Name {
srcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))
@@ -253,6 +259,10 @@ func FsArchiveDecompress(c *gin.Context) {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, srcPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcPaths = append(srcPaths, srcPath)
}
dstDir, err := user.JoinPath(req.DstDir)
@@ -260,8 +270,17 @@ func FsArchiveDecompress(c *gin.Context) {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, dstDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
tasks := make([]task.TaskExtensionInfo, 0, len(srcPaths))
for _, srcPath := range srcPaths {
perm := common.MergeRolePermissions(user, srcPath)
if !common.HasPermission(perm, common.PermDecompress) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
t, e := fs.ArchiveDecompress(c, srcPath, dstDir, model.ArchiveDecompressArgs{
ArchiveInnerArgs: model.ArchiveInnerArgs{
ArchiveArgs: model.ArchiveArgs{

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"encoding/base64"
"image/png"
"path"
"strings"
"time"
"github.com/Xhofe/go-cache"
@@ -89,13 +91,16 @@ func loginHash(c *gin.Context, req *LoginReq) {
type UserResp struct {
model.User
Otp bool `json:"otp"`
Otp bool `json:"otp"`
RoleNames []string `json:"role_names"`
Permissions []model.PermissionEntry `json:"permissions"`
}
// CurrentUser get current user by token
// if token is empty, return guest user
func CurrentUser(c *gin.Context) {
user := c.MustGet("user").(*model.User)
userResp := UserResp{
User: *user,
}
@@ -103,6 +108,30 @@ func CurrentUser(c *gin.Context) {
if userResp.OtpSecret != "" {
userResp.Otp = true
}
var roleNames []string
permMap := map[string]int32{}
addedPaths := map[string]bool{}
for _, role := range user.RolesDetail {
roleNames = append(roleNames, role.Name)
for _, entry := range role.PermissionScopes {
cleanPath := path.Clean("/" + strings.TrimPrefix(entry.Path, "/"))
permMap[cleanPath] |= entry.Permission
}
}
userResp.RoleNames = roleNames
for fullPath, perm := range permMap {
if !addedPaths[fullPath] {
userResp.Permissions = append(userResp.Permissions, model.PermissionEntry{
Path: fullPath,
Permission: perm,
})
addedPaths[fullPath] = true
}
}
common.SuccessResp(c, userResp)
}

View File

@@ -29,20 +29,29 @@ func FsRecursiveMove(c *gin.Context) {
}
user := c.MustGet("user").(*model.User)
if !user.CanMove() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcDir, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, srcDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
dstDir, err := user.JoinPath(req.DstDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, dstDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, srcDir)
if !common.HasPermission(perm, common.PermMove) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
meta, err := op.GetNearestMeta(srcDir)
if err != nil {
@@ -149,16 +158,20 @@ func FsBatchRename(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanRename() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqPath, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermRename) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
meta, err := op.GetNearestMeta(reqPath)
if err != nil {
@@ -194,14 +207,19 @@ func FsRegexRename(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanRename() {
reqPath, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqPath, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermRename) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}

View File

@@ -35,7 +35,12 @@ func FsMkdir(c *gin.Context) {
common.ErrorResp(c, err, 403)
return
}
if !user.CanWrite() {
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermWrite) {
meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
@@ -73,20 +78,29 @@ func FsMove(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanMove() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcDir, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, srcDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
dstDir, err := user.JoinPath(req.DstDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, dstDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
permMove := common.MergeRolePermissions(user, srcDir)
if !common.HasPermission(permMove, common.PermMove) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
if !req.Overwrite {
for _, name := range req.Names {
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
@@ -116,20 +130,29 @@ func FsCopy(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanCopy() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcDir, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, srcDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
dstDir, err := user.JoinPath(req.DstDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, dstDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, srcDir)
if !common.HasPermission(perm, common.PermCopy) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
if !req.Overwrite {
for _, name := range req.Names {
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
@@ -167,15 +190,20 @@ func FsRename(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanRename() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqPath, err := user.JoinPath(req.Path)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermRename) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
if !req.Overwrite {
dstPath := stdpath.Join(stdpath.Dir(reqPath), req.Name)
if dstPath != reqPath {
@@ -208,15 +236,20 @@ func FsRemove(c *gin.Context) {
return
}
user := c.MustGet("user").(*model.User)
if !user.CanRemove() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
reqDir, err := user.JoinPath(req.Dir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqDir)
if !common.HasPermission(perm, common.PermRemove) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
for _, name := range req.Names {
err := fs.Remove(c, stdpath.Join(reqDir, name))
if err != nil {
@@ -240,15 +273,20 @@ func FsRemoveEmptyDirectory(c *gin.Context) {
}
user := c.MustGet("user").(*model.User)
if !user.CanRemove() {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
srcDir, err := user.JoinPath(req.SrcDir)
if err != nil {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, srcDir) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, srcDir)
if !common.HasPermission(perm, common.PermRemove) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
meta, err := op.GetNearestMeta(srcDir)
if err != nil {

View File

@@ -48,12 +48,28 @@ type ObjResp struct {
}
type FsListResp struct {
Content []ObjResp `json:"content"`
Total int64 `json:"total"`
Readme string `json:"readme"`
Header string `json:"header"`
Write bool `json:"write"`
Provider string `json:"provider"`
Content []ObjLabelResp `json:"content"`
Total int64 `json:"total"`
Readme string `json:"readme"`
Header string `json:"header"`
Write bool `json:"write"`
Provider string `json:"provider"`
}
type ObjLabelResp struct {
Id string `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Modified time.Time `json:"modified"`
Created time.Time `json:"created"`
Sign string `json:"sign"`
Thumb string `json:"thumb"`
Type int `json:"type"`
HashInfoStr string `json:"hashinfo"`
HashInfo map[*utils.HashType]string `json:"hash_info"`
LabelList []model.Label `json:"label_list"`
}
func FsList(c *gin.Context) {
@@ -77,11 +93,12 @@ func FsList(c *gin.Context) {
}
}
c.Set("meta", meta)
if !common.CanAccess(user, meta, reqPath, req.Password) {
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return
}
if !user.CanWrite() && !common.CanWrite(meta, reqPath) && req.Refresh {
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermWrite) && !common.CanWrite(meta, reqPath) && req.Refresh {
common.ErrorStrResp(c, "Refresh without permission", 403)
return
}
@@ -90,7 +107,14 @@ 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 {
@@ -101,7 +125,7 @@ func FsList(c *gin.Context) {
Total: int64(total),
Readme: getReadme(meta, reqPath),
Header: getHeader(meta, reqPath),
Write: user.CanWrite() || common.CanWrite(meta, reqPath),
Write: common.HasPermission(perm, common.PermWrite) || common.CanWrite(meta, reqPath),
Provider: provider,
})
}
@@ -135,7 +159,7 @@ func FsDirs(c *gin.Context) {
}
}
c.Set("meta", meta)
if !common.CanAccess(user, meta, reqPath, req.Password) {
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return
}
@@ -144,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)
}
@@ -207,11 +238,25 @@ 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) []ObjResp {
var resp []ObjResp
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() {
labels = labelsByName[obj.GetName()]
}
thumb, _ := model.GetThumb(obj)
resp = append(resp, ObjResp{
resp = append(resp, ObjLabelResp{
Id: obj.GetID(),
Path: obj.GetPath(),
Name: obj.GetName(),
@@ -224,6 +269,7 @@ func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
Sign: common.Sign(obj, parent, encrypt),
Thumb: thumb,
Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
LabelList: labels,
})
}
return resp
@@ -236,11 +282,11 @@ type FsGetReq struct {
type FsGetResp struct {
ObjResp
RawURL string `json:"raw_url"`
Readme string `json:"readme"`
Header string `json:"header"`
Provider string `json:"provider"`
Related []ObjResp `json:"related"`
RawURL string `json:"raw_url"`
Readme string `json:"readme"`
Header string `json:"header"`
Provider string `json:"provider"`
Related []ObjLabelResp `json:"related"`
}
func FsGet(c *gin.Context) {
@@ -263,7 +309,7 @@ func FsGet(c *gin.Context) {
}
}
c.Set("meta", meta)
if !common.CanAccess(user, meta, reqPath, req.Password) {
if !common.CanAccessWithRoles(user, meta, reqPath, req.Password) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return
}
@@ -391,7 +437,7 @@ func FsOther(c *gin.Context) {
}
}
c.Set("meta", meta)
if !common.CanAccess(user, meta, req.Path, req.Password) {
if !common.CanAccessWithRoles(user, meta, req.Path, req.Password) {
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return
}

99
server/handles/label.go Normal file
View File

@@ -0,0 +1,99 @@
package handles
import (
"errors"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"strconv"
)
func ListLabel(c *gin.Context) {
var req model.PageReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Validate()
log.Debugf("%+v", req)
labels, total, err := db.GetLabels(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c, common.PageResp{
Content: labels,
Total: total,
})
}
func GetLabel(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
label, err := db.GetLabelById(uint(id))
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, label)
}
func CreateLabel(c *gin.Context) {
var req model.Label
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if db.GetLabelByName(req.Name) {
common.ErrorResp(c, errors.New("label name is exists"), 401)
return
}
if id, err := db.CreateLabel(req); err != nil {
common.ErrorWithDataResp(c, err, 500, gin.H{
"id": id,
}, true)
} else {
common.SuccessResp(c, gin.H{
"id": id,
})
}
}
func UpdateLabel(c *gin.Context) {
var req model.Label
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if label, err := db.UpdateLabel(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c, label)
}
}
func DeleteLabel(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
if err = op.DeleteLabelById(c, uint(id), userObj.ID); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}

View File

@@ -0,0 +1,250 @@
package handles
import (
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"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 {
FileName string `json:"file_name"`
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, decodedFileName)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, labels)
}
func CreateLabelFileBinDing(c *gin.Context) {
var req op.CreateLabelFileBinDingReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if req.IsDir == true {
common.ErrorStrResp(c, "Unable to bind folder", 400)
return
}
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
if err := op.CreateLabelFileBinDing(req, userObj.ID); err != nil {
common.ErrorResp(c, err, 500, true)
return
} else {
common.SuccessResp(c, gin.H{
"msg": "添加成功!",
})
}
}
func DelLabelByFileName(c *gin.Context) {
var req DelLabelFileBinDingReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
labelId, err := strconv.ParseUint(req.LabelId, 10, 64)
if err != nil {
common.ErrorResp(c, fmt.Errorf("invalid label ID '%s': %v", req.LabelId, err), 500, true)
return
}
if err = db.DelLabelFileBinDingById(uint(labelId), userObj.ID, req.FileName); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}
func GetFileByLabel(c *gin.Context) {
labelId := c.Query("label_id")
if labelId == "" {
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
return
}
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
fileList, err := op.GetFileByLabel(userObj.ID, labelId)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
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

@@ -131,7 +131,7 @@ func ladpRegister(username string) (*model.User, error) {
Password: random.String(16),
Permission: int32(setting.GetInt(conf.LdapDefaultPermission, 0)),
BasePath: setting.GetStr(conf.LdapDefaultDir),
Role: 0,
Role: nil,
Disabled: false,
}
if err := db.CreateUser(user); err != nil {

View File

@@ -5,6 +5,7 @@ import (
"github.com/alist-org/alist/v3/drivers/pikpak"
"github.com/alist-org/alist/v3/drivers/thunder"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/internal/op"
@@ -253,10 +254,6 @@ type AddOfflineDownloadReq struct {
func AddOfflineDownload(c *gin.Context) {
user := c.MustGet("user").(*model.User)
if !user.CanAddOfflineDownloadTasks() {
common.ErrorStrResp(c, "permission denied", 403)
return
}
var req AddOfflineDownloadReq
if err := c.ShouldBind(&req); err != nil {
@@ -268,6 +265,15 @@ func AddOfflineDownload(c *gin.Context) {
common.ErrorResp(c, err, 403)
return
}
if !common.CheckPathLimitWithRoles(user, reqPath) {
common.ErrorResp(c, errs.PermissionDenied, 403)
return
}
perm := common.MergeRolePermissions(user, reqPath)
if !common.HasPermission(perm, common.PermAddOfflineDownload) {
common.ErrorStrResp(c, "permission denied", 403)
return
}
var tasks []task.TaskExtensionInfo
for _, url := range req.Urls {
t, err := tool.AddURL(c, &tool.AddURLArgs{

105
server/handles/role.go Normal file
View File

@@ -0,0 +1,105 @@
package handles
import (
"strconv"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func ListRoles(c *gin.Context) {
var req model.PageReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Validate()
log.Debugf("%+v", req)
roles, total, err := op.GetRoles(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, common.PageResp{Content: roles, Total: total})
}
func GetRole(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
role, err := op.GetRole(uint(id))
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, role)
}
func CreateRole(c *gin.Context) {
var req model.Role
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.CreateRole(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}
func UpdateRole(c *gin.Context) {
var req model.Role
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
role, err := op.GetRole(req.ID)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
switch role.Name {
case "admin":
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
return
case "guest":
req.Name = "guest"
}
if err := op.UpdateRole(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}
func DeleteRole(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
role, err := op.GetRole(uint(id))
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
if role.Name == "admin" || role.Name == "guest" {
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
return
}
if err := op.DeleteRole(uint(id)); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}

View File

@@ -57,7 +57,7 @@ func Search(c *gin.Context) {
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
continue
}
if !common.CanAccess(user, meta, path.Join(node.Parent, node.Name), req.Password) {
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
continue
}
filteredNodes = append(filteredNodes, node)

View File

@@ -154,7 +154,7 @@ func autoRegister(username, userID string, err error) (*model.User, error) {
Password: random.String(16),
Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),
BasePath: setting.GetStr(conf.SSODefaultDir),
Role: 0,
Role: nil,
Disabled: false,
SsoID: userID,
}

View File

@@ -18,7 +18,7 @@ type TaskInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Creator string `json:"creator"`
CreatorRole int `json:"creator_role"`
CreatorRole model.Roles `json:"creator_role"`
State tache.State `json:"state"`
Status string `json:"status"`
Progress float64 `json:"progress"`
@@ -39,7 +39,7 @@ func getTaskInfo[T task.TaskExtensionInfo](task T) TaskInfo {
progress = 100
}
creatorName := ""
creatorRole := -1
var creatorRole model.Roles
if task.GetCreator() != nil {
creatorName = task.GetCreator().Username
creatorRole = task.GetCreator().Role

View File

@@ -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 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
@@ -74,9 +83,16 @@ func UpdateUser(c *gin.Context) {
if req.OtpSecret == "" {
req.OtpSecret = user.OtpSecret
}
if req.Disabled && req.IsAdmin() {
common.ErrorStrResp(c, "admin user can not be disabled", 400)
return
if req.Disabled && user.IsAdmin() {
count, err := op.CountEnabledAdminsExcluding(user.ID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
if count == 0 {
common.ErrorStrResp(c, "at least one enabled admin must be kept", 400)
return
}
}
if err := op.UpdateUser(&req); err != nil {
common.ErrorResp(c, err, 500)

View File

@@ -2,6 +2,7 @@ package middlewares
import (
"crypto/subtle"
"fmt"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
@@ -40,6 +41,15 @@ func Auth(c *gin.Context) {
c.Abort()
return
}
if len(guest.Role) > 0 {
roles, err := op.GetRolesByUserID(guest.ID)
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("Fail to load guest roles: %v", err), 500)
c.Abort()
return
}
guest.RolesDetail = roles
}
c.Set("user", guest)
log.Debugf("use empty token: %+v", guest)
c.Next()
@@ -68,6 +78,15 @@ func Auth(c *gin.Context) {
c.Abort()
return
}
if len(user.Role) > 0 {
roles, err := op.GetRolesByUserID(user.ID)
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("Fail to load roles: %v", err), 500)
c.Abort()
return
}
user.RolesDetail = roles
}
c.Set("user", user)
log.Debugf("use login token: %+v", user)
c.Next()
@@ -122,6 +141,19 @@ func Authn(c *gin.Context) {
c.Abort()
return
}
if len(user.Role) > 0 {
var roles []model.Role
for _, roleID := range user.Role {
role, err := op.GetRole(uint(roleID))
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("load role %d failed", roleID), 500)
c.Abort()
return
}
roles = append(roles, *role)
}
user.RolesDetail = roles
}
c.Set("user", user)
log.Debugf("use login token: %+v", user)
c.Next()

View File

@@ -35,7 +35,9 @@ func FsUp(c *gin.Context) {
return
}
}
if !(common.CanAccess(user, meta, path, password) && (user.CanWrite() || common.CanWrite(meta, stdpath.Dir(path)))) {
perm := common.MergeRolePermissions(user, path)
if !(common.CanAccessWithRoles(user, meta, path, password) &&
(common.HasPermission(perm, common.PermWrite) || common.CanWrite(meta, stdpath.Dir(path)))) {
common.ErrorResp(c, errs.PermissionDenied, 403)
c.Abort()
return

View File

@@ -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"))
@@ -120,6 +122,13 @@ func admin(g *gin.RouterGroup) {
user.GET("/sshkey/list", handles.ListPublicKeys)
user.POST("/sshkey/delete", handles.DeletePublicKey)
role := g.Group("/role")
role.GET("/list", handles.ListRoles)
role.GET("/get", handles.GetRole)
role.POST("/create", handles.CreateRole)
role.POST("/update", handles.UpdateRole)
role.POST("/delete", handles.DeleteRole)
storage := g.Group("/storage")
storage.GET("/list", handles.ListStorages)
storage.GET("/get", handles.GetStorage)
@@ -161,6 +170,19 @@ func admin(g *gin.RouterGroup) {
index.POST("/stop", middlewares.SearchIndex, handles.StopIndex)
index.POST("/clear", middlewares.SearchIndex, handles.ClearIndex)
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
label := g.Group("/label")
label.POST("/create", handles.CreateLabel)
label.POST("/update", handles.UpdateLabel)
label.POST("/delete", handles.DeleteLabel)
labelFileBinding := g.Group("/label_file_binding")
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) {
@@ -196,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

View File

@@ -8,6 +8,7 @@ import (
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/ftp"
"github.com/alist-org/alist/v3/server/sftp"
"github.com/pkg/errors"
@@ -78,7 +79,8 @@ func (d *SftpDriver) NoClientAuth(conn ssh.ConnMetadata) (*ssh.Permissions, erro
if err != nil {
return nil, err
}
if guest.Disabled || !guest.CanFTPAccess() {
permGuest := common.MergeRolePermissions(guest, guest.BasePath)
if guest.Disabled || !common.HasPermission(permGuest, common.PermFTPAccess) {
return nil, errors.New("user is not allowed to access via SFTP")
}
return nil, nil
@@ -89,7 +91,8 @@ func (d *SftpDriver) PasswordAuth(conn ssh.ConnMetadata, password []byte) (*ssh.
if err != nil {
return nil, err
}
if userObj.Disabled || !userObj.CanFTPAccess() {
perm := common.MergeRolePermissions(userObj, userObj.BasePath)
if userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {
return nil, errors.New("user is not allowed to access via SFTP")
}
passHash := model.StaticHash(string(password))
@@ -104,7 +107,8 @@ func (d *SftpDriver) PublicKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*s
if err != nil {
return nil, err
}
if userObj.Disabled || !userObj.CanFTPAccess() {
perm := common.MergeRolePermissions(userObj, userObj.BasePath)
if userObj.Disabled || !common.HasPermission(perm, common.PermFTPAccess) {
return nil, errors.New("user is not allowed to access via SFTP")
}
keys, _, err := op.GetSSHPublicKeyByUserId(userObj.ID, 1, -1)

View File

@@ -3,16 +3,19 @@ package server
import (
"context"
"crypto/subtle"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/server/middlewares"
"net/http"
"net/url"
"path"
"strings"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/server/middlewares"
"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/alist-org/alist/v3/server/webdav"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
@@ -92,7 +95,23 @@ func WebDAVAuth(c *gin.Context) {
c.Abort()
return
}
if user.Disabled || !user.CanWebdavRead() {
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
user.RolesDetail = roles
}
reqPath := c.Param("path")
if reqPath == "" {
reqPath = "/"
}
reqPath, _ = url.PathUnescape(reqPath)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
c.Status(http.StatusForbidden)
c.Abort()
return
}
perm := common.MergeRolePermissions(user, reqPath)
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()
@@ -102,27 +121,27 @@ func WebDAVAuth(c *gin.Context) {
c.Abort()
return
}
if (c.Request.Method == "PUT" || c.Request.Method == "MKCOL") && (!user.CanWebdavManage() || !user.CanWrite()) {
if (c.Request.Method == "PUT" || c.Request.Method == "MKCOL") && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermWrite)) {
c.Status(http.StatusForbidden)
c.Abort()
return
}
if c.Request.Method == "MOVE" && (!user.CanWebdavManage() || (!user.CanMove() && !user.CanRename())) {
if c.Request.Method == "MOVE" && (!common.HasPermission(perm, common.PermWebdavManage) || (!common.HasPermission(perm, common.PermMove) && !common.HasPermission(perm, common.PermRename))) {
c.Status(http.StatusForbidden)
c.Abort()
return
}
if c.Request.Method == "COPY" && (!user.CanWebdavManage() || !user.CanCopy()) {
if c.Request.Method == "COPY" && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermCopy)) {
c.Status(http.StatusForbidden)
c.Abort()
return
}
if c.Request.Method == "DELETE" && (!user.CanWebdavManage() || !user.CanRemove()) {
if c.Request.Method == "DELETE" && (!common.HasPermission(perm, common.PermWebdavManage) || !common.HasPermission(perm, common.PermRemove)) {
c.Status(http.StatusForbidden)
c.Abort()
return
}
if c.Request.Method == "PROPPATCH" && !user.CanWebdavManage() {
if c.Request.Method == "PROPPATCH" && !common.HasPermission(perm, common.PermWebdavManage) {
c.Status(http.StatusForbidden)
c.Abort()
return

View File

@@ -14,6 +14,7 @@ import (
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
)
// slashClean is equivalent to but slightly more efficient than
@@ -34,10 +35,11 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
srcName := path.Base(src)
dstName := path.Base(dst)
user := ctx.Value("user").(*model.User)
if srcDir != dstDir && !user.CanMove() {
perm := common.MergeRolePermissions(user, src)
if srcDir != dstDir && !common.HasPermission(perm, common.PermMove) {
return http.StatusForbidden, nil
}
if srcName != dstName && !user.CanRename() {
if srcName != dstName && !common.HasPermission(perm, common.PermRename) {
return http.StatusForbidden, nil
}
if srcDir == dstDir {
@@ -92,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)
@@ -106,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 += "/"
}