Compare commits

..

11 Commits

Author SHA1 Message Date
千石
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
alist666
9da56bab4d Merge pull request #9171 from AlistGo/fix-189pc-login
fix: update documentation links to point to the new domain And fix 189pc getToken fail
2025-06-28 00:20:50 +08:00
alistgo
51eeb22465 fix: dead link 2025-06-27 23:58:52 +08:00
Alone
b1586612ca feat: add ghcr docker image (#8524) 2025-06-27 23:39:23 +08:00
AlistDev
7aeb0ab078 fix: update documentation links to point to the new domain And fix 189pc getToken fail 2025-06-27 16:28:09 +08:00
MadDogOwner
ffa03bfda1 feat(cloudreve_v4): add Cloudreve V4 driver (#8470 closes #8328 #8467)
* feat(cloudreve_v4): add Cloudreve V4 driver implementation

* fix(cloudreve_v4): update request handling to prevent token refresh loop

* feat(onedrive): implement retry logic for upload failures

* feat(cloudreve): implement retry logic for upload failures

* feat(cloudreve_v4): support cloud sorting

* fix(cloudreve_v4): improve token handling in Init method

* feat(cloudreve_v4): support share

* feat(cloudreve): support reference

* feat(cloudreve_v4): support version upload

* fix(cloudreve_v4): add SetBody in upLocal

* fix(cloudreve_v4): update URL structure in Link and FileUrlResp
2025-05-24 13:38:43 +08:00
93 changed files with 3066 additions and 1028 deletions

2
.github/FUNDING.yml vendored
View File

@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://alist.nn.ci/guide/sponsor.html'] custom: ['https://alistgo.com/guide/sponsor.html']

View File

@@ -16,14 +16,14 @@ body:
您必须勾选以下所有内容否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/alist-org/alist/discussions) 您必须勾选以下所有内容否则您的issue可能会被直接关闭。或者您可以去[讨论区](https://github.com/alist-org/alist/discussions)
options: options:
- label: | - label: |
I have read the [documentation](https://alist.nn.ci). I have read the [documentation](https://alistgo.com).
我已经阅读了[文档](https://alist.nn.ci)。 我已经阅读了[文档](https://alistgo.com)。
- label: | - label: |
I'm sure there are no duplicate issues or discussions. I'm sure there are no duplicate issues or discussions.
我确定没有重复的issue或讨论。 我确定没有重复的issue或讨论。
- label: | - label: |
I'm sure it's due to `AList` and not something else(such as [Network](https://alist.nn.ci/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`). I'm sure it's due to `AList` and not something else(such as [Network](https://alistgo.com/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host) ,`Dependencies` or `Operational`).
我确定是`AList`的问题,而不是其他原因(例如[网络](https://alist.nn.ci/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)`依赖`或`操作`)。 我确定是`AList`的问题,而不是其他原因(例如[网络](https://alistgo.com/zh/faq/howto.html#tls-handshake-timeout-read-connection-reset-by-peer-dns-lookup-failed-connect-connection-refused-client-timeout-exceeded-while-awaiting-headers-no-such-host)`依赖`或`操作`)。
- label: | - label: |
I'm sure this issue is not fixed in the latest version. I'm sure this issue is not fixed in the latest version.
我确定这个问题在最新版本中没有被修复。 我确定这个问题在最新版本中没有被修复。

View File

@@ -7,7 +7,7 @@ body:
label: Please make sure of the following things label: Please make sure of the following things
description: You may select more than one, even select all. description: You may select more than one, even select all.
options: options:
- label: I have read the [documentation](https://alist.nn.ci). - label: I have read the [documentation](https://alistgo.com).
- label: I'm sure there are no duplicate issues or discussions. - label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented. - label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement. - label: I'm sure it's a reasonable and popular requirement.

View File

@@ -119,7 +119,7 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: alist-org/desktop-release repository: AlistGo/desktop-release
ref: main ref: main
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@@ -135,4 +135,4 @@ jobs:
with: with:
github_token: ${{ secrets.MY_TOKEN }} github_token: ${{ secrets.MY_TOKEN }}
branch: main branch: main
repository: alist-org/desktop-release repository: AlistGo/desktop-release

View File

@@ -72,7 +72,7 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: alist-org/desktop-release repository: AlistGo/desktop-release
ref: main ref: main
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
@@ -89,4 +89,4 @@ jobs:
with: with:
github_token: ${{ secrets.MY_TOKEN }} github_token: ${{ secrets.MY_TOKEN }}
branch: main branch: main
repository: alist-org/desktop-release repository: AlistGo/desktop-release

View File

@@ -18,6 +18,7 @@ env:
REGISTRY: 'xhofe/alist' REGISTRY: 'xhofe/alist'
REGISTRY_USERNAME: 'xhofe' REGISTRY_USERNAME: 'xhofe'
REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
GITHUB_CR_REPO: ghcr.io/${{ github.repository }}
ARTIFACT_NAME: 'binaries_docker_release' ARTIFACT_NAME: 'binaries_docker_release'
RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64' RELEASE_PLATFORMS: 'linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x,linux/ppc64le,linux/riscv64'
IMAGE_PUSH: ${{ github.event_name == 'push' }} IMAGE_PUSH: ${{ github.event_name == 'push' }}
@@ -114,11 +115,21 @@ jobs:
username: ${{ env.REGISTRY_USERNAME }} username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }} password: ${{ env.REGISTRY_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
logout: true
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }} images: |
${{ env.REGISTRY }}
${{ env.GITHUB_CR_REPO }}
tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }} tags: ${{ env.IMAGE_IS_PROD == 'true' && '' || env.IMAGE_TAGS_BETA }}
flavor: | flavor: |
${{ env.IMAGE_IS_PROD == 'true' && 'latest=true' || '' }} ${{ env.IMAGE_IS_PROD == 'true' && 'latest=true' || '' }}

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <a href="https://alistgo.com"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p> <p><em>🗂A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@@ -31,7 +31,7 @@
<a href="https://hub.docker.com/r/xhofe/alist"> <a href="https://hub.docker.com/r/xhofe/alist">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" /> <img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a> </a>
<a href="https://alist.nn.ci/guide/sponsor.html"> <a href="https://alistgo.com/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" /> <img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
@@ -88,7 +88,7 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
- [x] Dark mode - [x] Dark mode
- [x] I18n - [x] I18n
- [x] Protected routes (password protection and authentication) - [x] Protected routes (password protection and authentication)
- [x] WebDav (see https://alist.nn.ci/guide/webdav.html for details) - [x] WebDav (see https://alistgo.com/guide/webdav.html for details)
- [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist) - [x] [Docker Deploy](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare Workers proxy - [x] Cloudflare Workers proxy
- [x] File/Folder package download - [x] File/Folder package download
@@ -112,7 +112,7 @@ Please go to our [discussion forum](https://github.com/alist-org/alist/discussio
## Sponsor ## Sponsor
AList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support: AList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support:
https://alist.nn.ci/guide/sponsor.html https://alistgo.com/guide/sponsor.html
### Special sponsors ### Special sponsors

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <a href="https://alistgo.com"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p> <p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@@ -31,7 +31,7 @@
<a href="https://hub.docker.com/r/xhofe/alist"> <a href="https://hub.docker.com/r/xhofe/alist">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" /> <img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a> </a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html"> <a href="https://alistgo.com/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" /> <img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
@@ -86,7 +86,7 @@
- [x] 黑暗模式 - [x] 黑暗模式
- [x] 国际化 - [x] 国际化
- [x] 受保护的路由(密码保护和身份验证) - [x] 受保护的路由(密码保护和身份验证)
- [x] WebDav (具体见 https://alist.nn.ci/zh/guide/webdav.html) - [x] WebDav (具体见 https://alistgo.com/zh/guide/webdav.html)
- [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist) - [x] [Docker 部署](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare workers 中转 - [x] Cloudflare workers 中转
- [x] 文件/文件夹打包下载 - [x] 文件/文件夹打包下载
@@ -97,7 +97,7 @@
## 文档 ## 文档
<https://alist.nn.ci/zh/> <https://alistgo.com/zh/>
## Demo ## Demo
@@ -109,7 +109,7 @@
## 赞助 ## 赞助
AList 是一个开源软件如果你碰巧喜欢这个项目并希望我继续下去请考虑赞助我或提供一个单一的捐款感谢所有的爱和支持https://alist.nn.ci/zh/guide/sponsor.html AList 是一个开源软件如果你碰巧喜欢这个项目并希望我继续下去请考虑赞助我或提供一个单一的捐款感谢所有的爱和支持https://alistgo.com/zh/guide/sponsor.html
### 特别赞助 ### 特别赞助

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a> <a href="https://alistgo.com"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p> <p><em>🗂Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
<div> <div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3"> <a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@@ -31,7 +31,7 @@
<a href="https://hub.docker.com/r/xhofe/alist"> <a href="https://hub.docker.com/r/xhofe/alist">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" /> <img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a> </a>
<a href="https://alist.nn.ci/guide/sponsor.html"> <a href="https://alistgo.com/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" /> <img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a> </a>
</div> </div>
@@ -87,7 +87,7 @@
- [x] ダークモード - [x] ダークモード
- [x] 国際化 - [x] 国際化
- [x] 保護されたルート (パスワード保護と認証) - [x] 保護されたルート (パスワード保護と認証)
- [x] WebDav (詳細は https://alist.nn.ci/guide/webdav.html を参照) - [x] WebDav (詳細は https://alistgo.com/guide/webdav.html を参照)
- [x] [Docker デプロイ](https://hub.docker.com/r/xhofe/alist) - [x] [Docker デプロイ](https://hub.docker.com/r/xhofe/alist)
- [x] Cloudflare ワーカープロキシ - [x] Cloudflare ワーカープロキシ
- [x] ファイル/フォルダパッケージのダウンロード - [x] ファイル/フォルダパッケージのダウンロード
@@ -98,7 +98,7 @@
## ドキュメント ## ドキュメント
<https://alist.nn.ci/> <https://alistgo.com/>
## デモ ## デモ
@@ -111,7 +111,7 @@
## スポンサー ## スポンサー
AList はオープンソースのソフトウェアです。もしあなたがこのプロジェクトを気に入ってくださり、続けて欲しいと思ってくださるなら、ぜひスポンサーになってくださるか、1口でも寄付をしてくださるようご検討くださいすべての愛とサポートに感謝します: AList はオープンソースのソフトウェアです。もしあなたがこのプロジェクトを気に入ってくださり、続けて欲しいと思ってくださるなら、ぜひスポンサーになってくださるか、1口でも寄付をしてくださるようご検討くださいすべての愛とサポートに感謝します:
https://alist.nn.ci/guide/sponsor.html https://alistgo.com/guide/sponsor.html
### スペシャルスポンサー ### スペシャルスポンサー

View File

@@ -93,7 +93,7 @@ BuildDocker() {
PrepareBuildDockerMusl() { PrepareBuildDockerMusl() {
mkdir -p build/musl-libs mkdir -p build/musl-libs
BASE="https://musl.cc/" BASE="https://github.com/go-cross/musl-toolchain-archive/releases/latest/download/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross) FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross riscv64-linux-musl-cross powerpc64le-linux-musl-cross)
for i in "${FILES[@]}"; do for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz" url="${BASE}${i}.tgz"
@@ -245,7 +245,7 @@ BuildReleaseFreeBSD() {
cgo_cc="clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}" cgo_cc="clang --target=${CGO_ARGS[$i]} --sysroot=/opt/freebsd/${os_arch}"
echo building for freebsd-${os_arch} echo building for freebsd-${os_arch}
sudo mkdir -p "/opt/freebsd/${os_arch}" sudo mkdir -p "/opt/freebsd/${os_arch}"
wget -q https://download.freebsd.org/releases/${os_arch}/14.1-RELEASE/base.txz wget -q https://download.freebsd.org/releases/${os_arch}/14.3-RELEASE/base.txz
sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch} sudo tar -xf ./base.txz -C /opt/freebsd/${os_arch}
rm base.txz rm base.txz
export GOOS=freebsd export GOOS=freebsd

View File

@@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -16,6 +17,12 @@ func Init() {
bootstrap.InitConfig() bootstrap.InitConfig()
bootstrap.Log() bootstrap.Log()
bootstrap.InitDB() bootstrap.InitDB()
if v3_46_0.IsLegacyRoleDetected() {
utils.Log.Warnf("Detected legacy role format, executing ConvertLegacyRoles patch early...")
v3_46_0.ConvertLegacyRoles()
}
data.InitData() data.InitData()
bootstrap.InitStreamLimit() bootstrap.InitStreamLimit()
bootstrap.InitIndex() bootstrap.InitIndex()

View File

@@ -16,7 +16,7 @@ var RootCmd = &cobra.Command{
Short: "A file list program that supports multiple storage.", Short: "A file list program that supports multiple storage.",
Long: `A file list program that supports multiple storage, Long: `A file list program that supports multiple storage,
built with love by Xhofe and friends in Go/Solid.js. built with love by Xhofe and friends in Go/Solid.js.
Complete documentation is available at https://alist.nn.ci/`, Complete documentation is available at https://alistgo.com/`,
} }
func Execute() { func Execute() {
@@ -27,7 +27,7 @@ func Execute() {
} }
func init() { func init() {
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "/data", "data folder") RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode") RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix") RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode") RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")

View File

@@ -161,12 +161,12 @@ func (d *Pan123) login() error {
} }
res, err := base.RestyClient.R(). res, err := base.RestyClient.R().
SetHeaders(map[string]string{ SetHeaders(map[string]string{
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)-alist", //"user-agent": "Dart/2.19(dart:io)-alist",
"platform": "web", "platform": "web",
"app-version": "3", "app-version": "3",
//"user-agent": base.UserAgent, "user-agent": base.UserAgent,
}). }).
SetBody(body).Post(SignIn) SetBody(body).Post(SignIn)
if err != nil { if err != nil {
@@ -202,7 +202,7 @@ do:
"origin": "https://www.123pan.com", "origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/", "referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken, "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", "platform": "web",
"app-version": "3", "app-version": "3",
//"user-agent": base.UserAgent, //"user-agent": base.UserAgent,

View File

@@ -324,7 +324,7 @@ func (y *Cloud189PC) login() (err error) {
_, err = y.client.R(). _, err = y.client.R().
SetResult(&tokenInfo).SetError(&erron). SetResult(&tokenInfo).SetError(&erron).
SetQueryParams(clientSuffix()). SetQueryParams(clientSuffix()).
SetQueryParam("redirectURL", url.QueryEscape(loginresp.ToUrl)). SetQueryParam("redirectURL", loginresp.ToUrl).
Post(API_URL + "/getSessionForPC.action") Post(API_URL + "/getSessionForPC.action")
if err != nil { if err != nil {
return return

View File

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

View File

@@ -76,7 +76,7 @@ type MeResp struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
BasePath string `json:"base_path"` BasePath string `json:"base_path"`
Role int `json:"role"` Role []int `json:"role"`
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
Permission int `json:"permission"` Permission int `json:"permission"`
SsoId string `json:"sso_id"` SsoId string `json:"sso_id"`

View File

@@ -55,7 +55,7 @@ func (d *AliDrive) Init(ctx context.Context) error {
if err != nil { if err != nil {
return err 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.UserID = utils.Json.Get(res, "user_id").ToString()
d.cron = cron.NewCron(time.Hour * 2) d.cron = cron.NewCron(time.Hour * 2)
d.cron.Do(func() { d.cron.Do(func() {

View File

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

View File

@@ -22,6 +22,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/baidu_share" _ "github.com/alist-org/alist/v3/drivers/baidu_share"
_ "github.com/alist-org/alist/v3/drivers/chaoxing" _ "github.com/alist-org/alist/v3/drivers/chaoxing"
_ "github.com/alist-org/alist/v3/drivers/cloudreve" _ "github.com/alist-org/alist/v3/drivers/cloudreve"
_ "github.com/alist-org/alist/v3/drivers/cloudreve_v4"
_ "github.com/alist-org/alist/v3/drivers/crypt" _ "github.com/alist-org/alist/v3/drivers/crypt"
_ "github.com/alist-org/alist/v3/drivers/doubao" _ "github.com/alist-org/alist/v3/drivers/doubao"
_ "github.com/alist-org/alist/v3/drivers/doubao_share" _ "github.com/alist-org/alist/v3/drivers/doubao_share"

View File

@@ -18,6 +18,7 @@ import (
type Cloudreve struct { type Cloudreve struct {
model.Storage model.Storage
Addition Addition
ref *Cloudreve
} }
func (d *Cloudreve) Config() driver.Config { func (d *Cloudreve) Config() driver.Config {
@@ -37,8 +38,18 @@ func (d *Cloudreve) Init(ctx context.Context) error {
return d.login() return d.login()
} }
func (d *Cloudreve) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*Cloudreve)
if ok {
d.ref = refStorage
return nil
}
return errs.NotSupport
}
func (d *Cloudreve) Drop(ctx context.Context) error { func (d *Cloudreve) Drop(ctx context.Context) error {
d.Cookie = "" d.Cookie = ""
d.ref = nil
return nil return nil
} }

View File

@@ -4,12 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
@@ -19,7 +21,6 @@ import (
"github.com/alist-org/alist/v3/pkg/cookie" "github.com/alist-org/alist/v3/pkg/cookie"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
json "github.com/json-iterator/go"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@@ -35,6 +36,9 @@ func (d *Cloudreve) getUA() string {
} }
func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error { func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error {
if d.ref != nil {
return d.ref.request(method, path, callback, out)
}
u := d.Address + "/api/v3" + path u := d.Address + "/api/v3" + path
req := base.RestyClient.R() req := base.RestyClient.R()
req.SetHeaders(map[string]string{ req.SetHeaders(map[string]string{
@@ -79,11 +83,11 @@ func (d *Cloudreve) request(method string, path string, callback base.ReqCallbac
} }
if out != nil && r.Data != nil { if out != nil && r.Data != nil {
var marshal []byte var marshal []byte
marshal, err = json.Marshal(r.Data) marshal, err = jsoniter.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
err = json.Unmarshal(marshal, out) err = jsoniter.Unmarshal(marshal, out)
if err != nil { if err != nil {
return err return err
} }
@@ -187,12 +191,9 @@ func (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u Up
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
utils.Log.Debugf("[Cloudreve-Local] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[Cloudreve-Local] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n) utils.Log.Debug(err, n)
@@ -205,9 +206,26 @@ func (d *Cloudreve) upLocal(ctx context.Context, stream model.FileStreamer, u Up
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10)) req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
req.SetHeader("User-Agent", d.getUA()) req.SetHeader("User-Agent", d.getUA())
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData))) req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
req.AddRetryCondition(func(r *resty.Response, err error) bool {
if err != nil {
return true
}
if r.IsError() {
return true
}
var retryResp Resp
jErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)
if jErr != nil {
return true
}
if retryResp.Code != 0 {
return true
}
return false
})
}, nil) }, nil)
if err != nil { if err != nil {
break return err
} }
finish += byteSize finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize())) up(float64(finish) * 100 / float64(stream.GetSize()))
@@ -222,16 +240,15 @@ func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u U
var finish int64 = 0 var finish int64 = 0
var chunk int = 0 var chunk int = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
utils.Log.Debugf("[Cloudreve-Remote] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[Cloudreve-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n) utils.Log.Debug(err, n)
@@ -248,14 +265,43 @@ func (d *Cloudreve) upRemote(ctx context.Context, stream model.FileStreamer, u U
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize))) // req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Authorization", fmt.Sprint(credential)) req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA()) req.Header.Set("User-Agent", d.getUA())
finish += byteSize err = func() error {
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
}()
if err == nil {
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
} else {
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
time.Sleep(backoff)
} }
_ = res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
} }
return nil return nil
} }
@@ -264,16 +310,15 @@ func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u
uploadUrl := u.UploadURLs[0] uploadUrl := u.UploadURLs[0]
var finish int64 = 0 var finish int64 = 0
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
utils.Log.Debugf("[Cloudreve-OneDrive] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[Cloudreve-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n) utils.Log.Debug(err, n)
@@ -295,22 +340,31 @@ func (d *Cloudreve) upOneDrive(ctx context.Context, stream model.FileStreamer, u
return err return err
} }
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession // https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 { switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body) data, _ := io.ReadAll(res.Body)
_ = res.Body.Close() res.Body.Close()
return errors.New(string(data)) return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
_ = res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
// 上传成功发送回调请求 // 上传成功发送回调请求
err := d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) { return d.request(http.MethodPost, "/callback/onedrive/finish/"+u.SessionID, func(req *resty.Request) {
req.SetBody("{}") req.SetBody("{}")
}, nil) }, nil)
if err != nil {
return err
}
return nil
} }
func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error { func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u UploadInfo, up driver.UpdateProgress) error {
@@ -318,16 +372,15 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
var chunk int = 0 var chunk int = 0
var etags []string var etags []string
DEFAULT := int64(u.ChunkSize) DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
utils.Log.Debugf("[Cloudreve-S3] upload: %d", finish)
var byteSize = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[Cloudreve-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
utils.Log.Debug(err, n) utils.Log.Debug(err, n)
@@ -346,10 +399,26 @@ func (d *Cloudreve) upS3(ctx context.Context, stream model.FileStreamer, u Uploa
if err != nil { if err != nil {
return err return err
} }
_ = res.Body.Close() etag := res.Header.Get("ETag")
etags = append(etags, res.Header.Get("ETag")) res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize())) switch {
chunk++ case res.StatusCode != 200:
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-S3] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case etag == "":
return errors.New("faild to get ETag from header")
default:
retryCount = 0
etags = append(etags, etag)
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
chunk++
}
} }
// s3LikeFinishUpload // s3LikeFinishUpload

View File

@@ -0,0 +1,305 @@
package cloudreve_v4
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"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/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type CloudreveV4 struct {
model.Storage
Addition
ref *CloudreveV4
}
func (d *CloudreveV4) Config() driver.Config {
if d.ref != nil {
return d.ref.Config()
}
if d.EnableVersionUpload {
config.NoOverwriteUpload = false
}
return config
}
func (d *CloudreveV4) GetAddition() driver.Additional {
return &d.Addition
}
func (d *CloudreveV4) Init(ctx context.Context) error {
// removing trailing slash
d.Address = strings.TrimSuffix(d.Address, "/")
op.MustSaveDriverStorage(d)
if d.ref != nil {
return nil
}
if d.AccessToken == "" && d.RefreshToken != "" {
return d.refreshToken()
}
if d.Username != "" {
return d.login()
}
return nil
}
func (d *CloudreveV4) InitReference(storage driver.Driver) error {
refStorage, ok := storage.(*CloudreveV4)
if ok {
d.ref = refStorage
return nil
}
return errs.NotSupport
}
func (d *CloudreveV4) Drop(ctx context.Context) error {
d.ref = nil
return nil
}
func (d *CloudreveV4) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
const pageSize int = 100
var f []File
var r FileResp
params := map[string]string{
"page_size": strconv.Itoa(pageSize),
"uri": dir.GetPath(),
"order_by": d.OrderBy,
"order_direction": d.OrderDirection,
"page": "0",
}
for {
err := d.request(http.MethodGet, "/file", func(req *resty.Request) {
req.SetQueryParams(params)
}, &r)
if err != nil {
return nil, err
}
f = append(f, r.Files...)
if r.Pagination.NextToken == "" || len(r.Files) < pageSize {
break
}
params["next_page_token"] = r.Pagination.NextToken
}
return utils.SliceConvert(f, func(src File) (model.Obj, error) {
if d.EnableFolderSize && src.Type == 1 {
var ds FolderSummaryResp
err := d.request(http.MethodGet, "/file/info", func(req *resty.Request) {
req.SetQueryParam("uri", src.Path)
req.SetQueryParam("folder_summary", "true")
}, &ds)
if err == nil && ds.FolderSummary.Size > 0 {
src.Size = ds.FolderSummary.Size
}
}
var thumb model.Thumbnail
if d.EnableThumb && src.Type == 0 {
var t FileThumbResp
err := d.request(http.MethodGet, "/file/thumb", func(req *resty.Request) {
req.SetQueryParam("uri", src.Path)
}, &t)
if err == nil && t.URL != "" {
thumb = model.Thumbnail{
Thumbnail: t.URL,
}
}
}
return &model.ObjThumb{
Object: model.Object{
ID: src.ID,
Path: src.Path,
Name: src.Name,
Size: src.Size,
Modified: src.UpdatedAt,
Ctime: src.CreatedAt,
IsFolder: src.Type == 1,
},
Thumbnail: thumb,
}, nil
})
}
func (d *CloudreveV4) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var url FileUrlResp
err := d.request(http.MethodPost, "/file/url", func(req *resty.Request) {
req.SetBody(base.Json{
"uris": []string{file.GetPath()},
"download": true,
})
}, &url)
if err != nil {
return nil, err
}
if len(url.Urls) == 0 {
return nil, errors.New("server returns no url")
}
exp := time.Until(url.Expires)
return &model.Link{
URL: url.Urls[0].URL,
Expiration: &exp,
}, nil
}
func (d *CloudreveV4) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
req.SetBody(base.Json{
"type": "folder",
"uri": parentDir.GetPath() + "/" + dirName,
"error_on_conflict": true,
})
}, nil)
}
func (d *CloudreveV4) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.request(http.MethodPost, "/file/move", func(req *resty.Request) {
req.SetBody(base.Json{
"uris": []string{srcObj.GetPath()},
"dst": dstDir.GetPath(),
"copy": false,
})
}, nil)
}
func (d *CloudreveV4) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
req.SetBody(base.Json{
"new_name": newName,
"uri": srcObj.GetPath(),
})
}, nil)
}
func (d *CloudreveV4) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.request(http.MethodPost, "/file/move", func(req *resty.Request) {
req.SetBody(base.Json{
"uris": []string{srcObj.GetPath()},
"dst": dstDir.GetPath(),
"copy": true,
})
}, nil)
}
func (d *CloudreveV4) Remove(ctx context.Context, obj model.Obj) error {
return d.request(http.MethodDelete, "/file", func(req *resty.Request) {
req.SetBody(base.Json{
"uris": []string{obj.GetPath()},
"unlink": false,
"skip_soft_delete": true,
})
}, nil)
}
func (d *CloudreveV4) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
if file.GetSize() == 0 {
// 空文件使用新建文件方法,避免上传卡锁
return d.request(http.MethodPost, "/file/create", func(req *resty.Request) {
req.SetBody(base.Json{
"type": "file",
"uri": dstDir.GetPath() + "/" + file.GetName(),
"error_on_conflict": true,
})
}, nil)
}
var p StoragePolicy
var r FileResp
var u FileUploadResp
var err error
params := map[string]string{
"page_size": "10",
"uri": dstDir.GetPath(),
"order_by": "created_at",
"order_direction": "asc",
"page": "0",
}
err = d.request(http.MethodGet, "/file", func(req *resty.Request) {
req.SetQueryParams(params)
}, &r)
if err != nil {
return err
}
p = r.StoragePolicy
body := base.Json{
"uri": dstDir.GetPath() + "/" + file.GetName(),
"size": file.GetSize(),
"policy_id": p.ID,
"last_modified": file.ModTime().UnixMilli(),
"mime_type": "",
}
if d.EnableVersionUpload {
body["entity_type"] = "version"
}
err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) {
req.SetBody(body)
}, &u)
if err != nil {
return err
}
if u.StoragePolicy.Relay {
err = d.upLocal(ctx, file, u, up)
} else {
switch u.StoragePolicy.Type {
case "local":
err = d.upLocal(ctx, file, u, up)
case "remote":
err = d.upRemote(ctx, file, u, up)
case "onedrive":
err = d.upOneDrive(ctx, file, u, up)
case "s3":
err = d.upS3(ctx, file, u, up)
default:
return errs.NotImplement
}
}
if err != nil {
// 删除失败的会话
_ = d.request(http.MethodDelete, "/file/upload", func(req *resty.Request) {
req.SetBody(base.Json{
"id": u.SessionID,
"uri": u.URI,
})
}, nil)
return err
}
return nil
}
func (d *CloudreveV4) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
return nil, errs.NotImplement
}
func (d *CloudreveV4) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
return nil, errs.NotImplement
}
func (d *CloudreveV4) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
return nil, errs.NotImplement
}
func (d *CloudreveV4) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
// return errs.NotImplement to use an internal archive tool
return nil, errs.NotImplement
}
//func (d *CloudreveV4) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*CloudreveV4)(nil)

View File

@@ -0,0 +1,44 @@
package cloudreve_v4
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
driver.RootPath
// driver.RootID
// define other
Address string `json:"address" required:"true"`
Username string `json:"username"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
CustomUA string `json:"custom_ua"`
EnableFolderSize bool `json:"enable_folder_size"`
EnableThumb bool `json:"enable_thumb"`
EnableVersionUpload bool `json:"enable_version_upload"`
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at" default:"name" required:"true"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc" required:"true"`
}
var config = driver.Config{
Name: "Cloudreve V4",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "cloudreve://my",
CheckStatus: true,
Alert: "",
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &CloudreveV4{}
})
}

View File

@@ -0,0 +1,164 @@
package cloudreve_v4
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type Object struct {
model.Object
StoragePolicy StoragePolicy
}
type Resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
type BasicConfigResp struct {
InstanceID string `json:"instance_id"`
// Title string `json:"title"`
// Themes string `json:"themes"`
// DefaultTheme string `json:"default_theme"`
User struct {
ID string `json:"id"`
// Nickname string `json:"nickname"`
// CreatedAt time.Time `json:"created_at"`
// Anonymous bool `json:"anonymous"`
Group struct {
ID string `json:"id"`
Name string `json:"name"`
Permission string `json:"permission"`
} `json:"group"`
} `json:"user"`
// Logo string `json:"logo"`
// LogoLight string `json:"logo_light"`
// CaptchaReCaptchaKey string `json:"captcha_ReCaptchaKey"`
CaptchaType string `json:"captcha_type"` // support 'normal' only
// AppPromotion bool `json:"app_promotion"`
}
type SiteLoginConfigResp struct {
LoginCaptcha bool `json:"login_captcha"`
Authn bool `json:"authn"`
}
type PrepareLoginResp struct {
WebauthnEnabled bool `json:"webauthn_enabled"`
PasswordEnabled bool `json:"password_enabled"`
}
type CaptchaResp struct {
Image string `json:"image"`
Ticket string `json:"ticket"`
}
type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccessExpires time.Time `json:"access_expires"`
RefreshExpires time.Time `json:"refresh_expires"`
}
type TokenResponse struct {
User struct {
ID string `json:"id"`
// Email string `json:"email"`
// Nickname string `json:"nickname"`
Status string `json:"status"`
// CreatedAt time.Time `json:"created_at"`
Group struct {
ID string `json:"id"`
Name string `json:"name"`
Permission string `json:"permission"`
// DirectLinkBatchSize int `json:"direct_link_batch_size"`
// TrashRetention int `json:"trash_retention"`
} `json:"group"`
// Language string `json:"language"`
} `json:"user"`
Token Token `json:"token"`
}
type File struct {
Type int `json:"type"` // 0: file, 1: folder
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Size int64 `json:"size"`
Metadata interface{} `json:"metadata"`
Path string `json:"path"`
Capability string `json:"capability"`
Owned bool `json:"owned"`
PrimaryEntity string `json:"primary_entity"`
}
type StoragePolicy struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
MaxSize int64 `json:"max_size"`
Relay bool `json:"relay,omitempty"`
}
type Pagination struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
IsCursor bool `json:"is_cursor"`
NextToken string `json:"next_token,omitempty"`
}
type Props struct {
Capability string `json:"capability"`
MaxPageSize int `json:"max_page_size"`
OrderByOptions []string `json:"order_by_options"`
OrderDirectionOptions []string `json:"order_direction_options"`
}
type FileResp struct {
Files []File `json:"files"`
Parent File `json:"parent"`
Pagination Pagination `json:"pagination"`
Props Props `json:"props"`
ContextHint string `json:"context_hint"`
MixedType bool `json:"mixed_type"`
StoragePolicy StoragePolicy `json:"storage_policy"`
}
type FileUrlResp struct {
Urls []struct {
URL string `json:"url"`
} `json:"urls"`
Expires time.Time `json:"expires"`
}
type FileUploadResp struct {
// UploadID string `json:"upload_id"`
SessionID string `json:"session_id"`
ChunkSize int64 `json:"chunk_size"`
Expires int64 `json:"expires"`
StoragePolicy StoragePolicy `json:"storage_policy"`
URI string `json:"uri"`
CompleteURL string `json:"completeURL,omitempty"` // for S3-like
CallbackSecret string `json:"callback_secret,omitempty"` // for S3-like, OneDrive
UploadUrls []string `json:"upload_urls,omitempty"` // for not-local
Credential string `json:"credential,omitempty"` // for local
}
type FileThumbResp struct {
URL string `json:"url"`
Expires time.Time `json:"expires"`
}
type FolderSummaryResp struct {
File
FolderSummary struct {
Size int64 `json:"size"`
Files int64 `json:"files"`
Folders int64 `json:"folders"`
Completed bool `json:"completed"`
CalculatedAt time.Time `json:"calculated_at"`
} `json:"folder_summary"`
}

View File

@@ -0,0 +1,476 @@
package cloudreve_v4
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"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/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
// do others that not defined in Driver interface
func (d *CloudreveV4) getUA() string {
if d.CustomUA != "" {
return d.CustomUA
}
return base.UserAgent
}
func (d *CloudreveV4) request(method string, path string, callback base.ReqCallback, out any) error {
if d.ref != nil {
return d.ref.request(method, path, callback, out)
}
u := d.Address + "/api/v4" + path
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"Accept": "application/json, text/plain, */*",
"User-Agent": d.getUA(),
})
if d.AccessToken != "" {
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
}
var r Resp
req.SetResult(&r)
if callback != nil {
callback(req)
}
resp, err := req.Execute(method, u)
if err != nil {
return err
}
if !resp.IsSuccess() {
return errors.New(resp.String())
}
if r.Code != 0 {
if r.Code == 401 && d.RefreshToken != "" && path != "/session/token/refresh" {
// try to refresh token
err = d.refreshToken()
if err != nil {
return err
}
return d.request(method, path, callback, out)
}
return errors.New(r.Msg)
}
if out != nil && r.Data != nil {
var marshal []byte
marshal, err = json.Marshal(r.Data)
if err != nil {
return err
}
err = json.Unmarshal(marshal, out)
if err != nil {
return err
}
}
return nil
}
func (d *CloudreveV4) login() error {
var siteConfig SiteLoginConfigResp
err := d.request(http.MethodGet, "/site/config/login", nil, &siteConfig)
if err != nil {
return err
}
if !siteConfig.Authn {
return errors.New("authn not support")
}
var prepareLogin PrepareLoginResp
err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
if err != nil {
return err
}
if !prepareLogin.PasswordEnabled {
return errors.New("password not enabled")
}
if prepareLogin.WebauthnEnabled {
return errors.New("webauthn not support")
}
for range 5 {
err = d.doLogin(siteConfig.LoginCaptcha)
if err == nil {
break
}
if err.Error() != "CAPTCHA not match." {
break
}
}
return err
}
func (d *CloudreveV4) doLogin(needCaptcha bool) error {
var err error
loginBody := base.Json{
"email": d.Username,
"password": d.Password,
}
if needCaptcha {
var config BasicConfigResp
err = d.request(http.MethodGet, "/site/config/basic", nil, &config)
if err != nil {
return err
}
if config.CaptchaType != "normal" {
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
}
var captcha CaptchaResp
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
if err != nil {
return err
}
if !strings.HasPrefix(captcha.Image, "data:image/png;base64,") {
return errors.New("can not get captcha")
}
loginBody["ticket"] = captcha.Ticket
i := strings.Index(captcha.Image, ",")
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(captcha.Image[i+1:]))
vRes, err := base.RestyClient.R().SetMultipartField(
"image", "validateCode.png", "image/png", dec).
Post(setting.GetStr(conf.OcrApi))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 {
return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString())
}
captchaCode := jsoniter.Get(vRes.Body(), "result").ToString()
if captchaCode == "" {
return errors.New("ocr error: empty result")
}
loginBody["captcha"] = captchaCode
}
var token TokenResponse
err = d.request(http.MethodPost, "/session/token", func(req *resty.Request) {
req.SetBody(loginBody)
}, &token)
if err != nil {
return err
}
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
op.MustSaveDriverStorage(d)
return nil
}
func (d *CloudreveV4) refreshToken() error {
var token Token
if token.RefreshToken == "" {
if d.Username != "" {
err := d.login()
if err != nil {
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
}
}
return nil
}
err := d.request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
req.SetBody(base.Json{
"refresh_token": d.RefreshToken,
})
}, &token)
if err != nil {
return err
}
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
op.MustSaveDriverStorage(d)
return nil
}
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
var finish int64 = 0
var chunk int = 0
DEFAULT := int64(u.ChunkSize)
if DEFAULT == 0 {
// support relay
DEFAULT = file.GetSize()
}
for finish < file.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
left := file.GetSize() - finish
byteSize := min(left, DEFAULT)
utils.Log.Debugf("[CloudreveV4-Local] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) {
req.SetHeader("Content-Type", "application/octet-stream")
req.SetContentLength(true)
req.SetHeader("Content-Length", strconv.FormatInt(byteSize, 10))
req.SetBody(driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
req.AddRetryCondition(func(r *resty.Response, err error) bool {
if err != nil {
return true
}
if r.IsError() {
return true
}
var retryResp Resp
jErr := base.RestyClient.JSONUnmarshal(r.Body(), &retryResp)
if jErr != nil {
return true
}
if retryResp.Code != 0 {
return true
}
return false
})
}, nil)
if err != nil {
return err
}
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
chunk++
}
return nil
}
func (d *CloudreveV4) upRemote(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
uploadUrl := u.UploadUrls[0]
credential := u.Credential
var finish int64 = 0
var chunk int = 0
DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
left := file.GetSize() - finish
byteSize := min(left, DEFAULT)
utils.Log.Debugf("[CloudreveV4-Remote] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest("POST", uploadUrl+"?chunk="+strconv.Itoa(chunk),
driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Authorization", fmt.Sprint(credential))
req.Header.Set("User-Agent", d.getUA())
err = func() error {
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var up Resp
err = json.Unmarshal(body, &up)
if err != nil {
return err
}
if up.Code != 0 {
return errors.New(up.Msg)
}
return nil
}()
if err == nil {
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
chunk++
} else {
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors, error: %s", maxRetries, err)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Cloudreve-Remote] server errors while uploading, retrying after %v...", backoff)
time.Sleep(backoff)
}
}
return nil
}
func (d *CloudreveV4) upOneDrive(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
uploadUrl := u.UploadUrls[0]
var finish int64 = 0
DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
left := file.GetSize() - finish
byteSize := min(left, DEFAULT)
utils.Log.Debugf("[CloudreveV4-OneDrive] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, uploadUrl, driver.NewLimitedUploadStream(ctx, bytes.NewReader(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, file.GetSize()))
req.Header.Set("User-Agent", d.getUA())
finish += byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[CloudreveV4-OneDrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
}
}
// 上传成功发送回调请求
return d.request(http.MethodPost, "/callback/onedrive/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
req.SetBody("{}")
}, nil)
}
func (d *CloudreveV4) upS3(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
var finish int64 = 0
var chunk int = 0
var etags []string
DEFAULT := int64(u.ChunkSize)
retryCount := 0
maxRetries := 3
for finish < file.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
left := file.GetSize() - finish
byteSize := min(left, DEFAULT)
utils.Log.Debugf("[CloudreveV4-S3] upload range: %d-%d/%d", finish, finish+byteSize-1, file.GetSize())
byteData := make([]byte, byteSize)
n, err := io.ReadFull(file, byteData)
utils.Log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, u.UploadUrls[chunk],
driver.NewLimitedUploadStream(ctx, bytes.NewBuffer(byteData)))
if err != nil {
return err
}
req = req.WithContext(ctx)
req.ContentLength = byteSize
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
etag := res.Header.Get("ETag")
res.Body.Close()
switch {
case res.StatusCode != 200:
retryCount++
if retryCount > maxRetries {
return fmt.Errorf("upload failed after %d retries due to server errors", maxRetries)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("server error %d, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case etag == "":
return errors.New("faild to get ETag from header")
default:
retryCount = 0
etags = append(etags, etag)
finish += byteSize
up(float64(finish) * 100 / float64(file.GetSize()))
chunk++
}
}
// s3LikeFinishUpload
bodyBuilder := &strings.Builder{}
bodyBuilder.WriteString("<CompleteMultipartUpload>")
for i, etag := range etags {
bodyBuilder.WriteString(fmt.Sprintf(
`<Part><PartNumber>%d</PartNumber><ETag>%s</ETag></Part>`,
i+1, // PartNumber 从 1 开始
etag,
))
}
bodyBuilder.WriteString("</CompleteMultipartUpload>")
req, err := http.NewRequest(
"POST",
u.CompleteURL,
strings.NewReader(bodyBuilder.String()),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/xml")
req.Header.Set("User-Agent", d.getUA())
res, err := base.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("up status: %d, error: %s", res.StatusCode, string(body))
}
// 上传成功发送回调请求
return d.request(http.MethodPost, "/callback/s3/"+u.SessionID+"/"+u.CallbackSecret, func(req *resty.Request) {
req.SetBody("{}")
}, nil)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"sync"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
@@ -36,88 +37,130 @@ func (d *GithubReleases) Drop(ctx context.Context) error {
return nil 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) { func (d *GithubReleases) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files := make([]File, 0) files := make([]File, 0)
path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/")) path := fmt.Sprintf("/%s", strings.Trim(dir.GetPath(), "/"))
for i := range d.points { if d.Addition.ConcurrentRequests && d.Addition.Token != "" { // 并发处理
point := &d.points[i] var mu sync.Mutex
var wg sync.WaitGroup
if !d.Addition.ShowAllVersion { // latest for i := range d.points {
point.RequestRelease(d.GetRequest, args.Refresh) wg.Add(1)
go func(point *MountPoint) {
defer wg.Done()
pointFiles := d.processPoint(point, path, args)
if point.Point == path { // 与仓库路径相同 mu.Lock()
files = append(files, point.GetLatestRelease()...) d.mergeFiles(&files, pointFiles)
if d.Addition.ShowReadme { mu.Unlock()
files = append(files, point.GetOtherFile(d.GetRequest, args.Refresh)...) }(&d.points[i])
} }
} else if strings.HasPrefix(point.Point, path) { // 仓库目录的父目录 wg.Wait()
nextDir := GetNextDir(point.Point, path) } else { // 串行处理
if nextDir == "" { for i := range d.points {
continue point := &d.points[i]
} pointFiles := d.processPoint(point, path, args)
d.mergeFiles(&files, pointFiles)
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)...)
}
} }
} }

View File

@@ -7,11 +7,12 @@ import (
type Addition struct { type Addition struct {
driver.RootID driver.RootID
RepoStructure string `json:"repo_structure" type:"text" required:"true" default:"alistGo/alist" help:"structure:[path:]org/repo"` 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"` 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"` 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"` 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 "` 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{ 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 == "" { if m.Repo == "" {
return return
} }

View File

@@ -6,8 +6,8 @@ import (
"strings" "strings"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
) )
// 发送 GET 请求 // 发送 GET 请求
@@ -23,7 +23,7 @@ func (d *GithubReleases) GetRequest(url string) (*resty.Response, error) {
return nil, err return nil, err
} }
if res.StatusCode() != 200 { 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 return res, nil
} }

View File

@@ -11,7 +11,7 @@ type Addition struct {
IsSharepoint bool `json:"is_sharepoint"` IsSharepoint bool `json:"is_sharepoint"`
ClientID string `json:"client_id" required:"true"` ClientID string `json:"client_id" required:"true"`
ClientSecret string `json:"client_secret" required:"true"` ClientSecret string `json:"client_secret" required:"true"`
RedirectUri string `json:"redirect_uri" required:"true" default:"https://alist.nn.ci/tool/onedrive/callback"` RedirectUri string `json:"redirect_uri" required:"true" default:"https://alistgo.com/tool/onedrive/callback"`
RefreshToken string `json:"refresh_token" required:"true"` RefreshToken string `json:"refresh_token" required:"true"`
SiteId string `json:"site_id"` SiteId string `json:"site_id"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"` ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
stdpath "path" stdpath "path"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@@ -17,7 +18,6 @@ import (
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
) )
var onedriveHostMap = map[string]Host{ var onedriveHostMap = map[string]Host{
@@ -204,19 +204,18 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString() uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0 var finish int64 = 0
DEFAULT := d.ChunkSize * 1024 * 1024 DEFAULT := d.ChunkSize * 1024 * 1024
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
log.Debugf("upload: %d", finish)
var byteSize int64 = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[Onedrive] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
log.Debug(err, n) utils.Log.Debug(err, n)
if err != nil { if err != nil {
return err return err
} }
@@ -228,19 +227,31 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
req.ContentLength = byteSize req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize))) // req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())) req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession // https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 { switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[Onedrive] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body) data, _ := io.ReadAll(res.Body)
res.Body.Close() res.Body.Close()
return errors.New(string(data)) return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
return nil return nil
} }

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
stdpath "path" stdpath "path"
"time"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
@@ -17,7 +18,6 @@ import (
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
) )
var onedriveHostMap = map[string]Host{ var onedriveHostMap = map[string]Host{
@@ -154,19 +154,18 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString() uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0 var finish int64 = 0
DEFAULT := d.ChunkSize * 1024 * 1024 DEFAULT := d.ChunkSize * 1024 * 1024
retryCount := 0
maxRetries := 3
for finish < stream.GetSize() { for finish < stream.GetSize() {
if utils.IsCanceled(ctx) { if utils.IsCanceled(ctx) {
return ctx.Err() return ctx.Err()
} }
log.Debugf("upload: %d", finish)
var byteSize int64 = DEFAULT
left := stream.GetSize() - finish left := stream.GetSize() - finish
if left < DEFAULT { byteSize := min(left, DEFAULT)
byteSize = left utils.Log.Debugf("[OnedriveAPP] upload range: %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())
}
byteData := make([]byte, byteSize) byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData) n, err := io.ReadFull(stream, byteData)
log.Debug(err, n) utils.Log.Debug(err, n)
if err != nil { if err != nil {
return err return err
} }
@@ -178,19 +177,31 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.
req.ContentLength = byteSize req.ContentLength = byteSize
// req.Header.Set("Content-Length", strconv.Itoa(int(byteSize))) // req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize())) req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize
res, err := base.HttpClient.Do(req) res, err := base.HttpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
// https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession // https://learn.microsoft.com/zh-cn/onedrive/developer/rest-api/api/driveitem_createuploadsession
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 { switch {
case res.StatusCode >= 500 && res.StatusCode <= 504:
retryCount++
if retryCount > maxRetries {
res.Body.Close()
return fmt.Errorf("upload failed after %d retries due to server errors, error %d", maxRetries, res.StatusCode)
}
backoff := time.Duration(1<<retryCount) * time.Second
utils.Log.Warnf("[OnedriveAPP] server errors %d while uploading, retrying after %v...", res.StatusCode, backoff)
time.Sleep(backoff)
case res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200:
data, _ := io.ReadAll(res.Body) data, _ := io.ReadAll(res.Body)
res.Body.Close() res.Body.Close()
return errors.New(string(data)) return errors.New(string(data))
default:
res.Body.Close()
retryCount = 0
finish += byteSize
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
res.Body.Close()
up(float64(finish) * 100 / float64(stream.GetSize()))
} }
return nil return nil
} }

View File

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

11
go.mod
View File

@@ -3,8 +3,6 @@ module github.com/alist-org/alist/v3
go 1.23.4 go 1.23.4
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0 github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
github.com/KirCute/sftpd-alist v0.0.12 github.com/KirCute/sftpd-alist v0.0.12
github.com/ProtonMail/go-crypto v1.0.0 github.com/ProtonMail/go-crypto v1.0.0
@@ -75,7 +73,6 @@ require (
golang.org/x/time v0.8.0 golang.org/x/time v0.8.0
google.golang.org/appengine v1.6.8 google.golang.org/appengine v1.6.8
gopkg.in/ldap.v3 v3.1.0 gopkg.in/ldap.v3 v3.1.0
gorm.io/datatypes v1.2.5
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/sqlite v1.5.6
@@ -83,8 +80,9 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
) )
require ( require (
@@ -111,6 +109,7 @@ require (
github.com/ipfs/boxo v0.12.0 // indirect github.com/ipfs/boxo v0.12.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78
@@ -168,7 +167,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-webauthn/x v0.1.12 // indirect github.com/go-webauthn/x v0.1.12 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
@@ -182,7 +181,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-cid v0.4.1
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect

32
go.sum
View File

@@ -19,20 +19,12 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -180,6 +172,7 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -252,9 +245,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU=
github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-webauthn/webauthn v0.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA= github.com/go-webauthn/webauthn v0.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA=
github.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE= github.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE=
github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A= github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A=
@@ -267,10 +259,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -359,8 +347,8 @@ github.com/ipfs/go-ipfs-api v0.7.0 h1:CMBNCUl0b45coC+lQCXEVpMhwoqjiaCwUIrM+coYW2
github.com/ipfs/go-ipfs-api v0.7.0/go.mod h1:AIxsTNB0+ZhkqIfTZpdZ0VR/cpX5zrXjATa3prSay3g= github.com/ipfs/go-ipfs-api v0.7.0/go.mod h1:AIxsTNB0+ZhkqIfTZpdZ0VR/cpX5zrXjATa3prSay3g=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
@@ -410,8 +398,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc= github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -450,8 +436,6 @@ github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc= github.com/minio/sio v0.4.0 h1:u4SWVEm5lXSqU42ZWawV0D9I5AZ5YMmo2RXpEQ/kRhc=
@@ -508,8 +492,6 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -757,6 +739,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -962,16 +946,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -3,11 +3,10 @@ package data
import "github.com/alist-org/alist/v3/cmd/flags" import "github.com/alist-org/alist/v3/cmd/flags"
func InitData() { func InitData() {
initRoles()
initUser() initUser()
initSettings() initSettings()
initTasks() initTasks()
initPermissions()
initRoles()
if flags.Dev { if flags.Dev {
initDevData() initDevData()
initDevDo() initDevDo()

View File

@@ -23,11 +23,10 @@ func initDevData() {
log.Fatalf("failed to create storage: %+v", err) log.Fatalf("failed to create storage: %+v", err)
} }
err = db.CreateUser(&model.User{ err = db.CreateUser(&model.User{
Username: "Noah", Username: "Noah",
Password: "hsu", Password: "hsu",
BasePath: "/data", BasePath: "/data",
//Role: []int{0}, Role: nil,
RoleInfo: []uint{0},
Permission: 512, Permission: 512,
}) })
if err != nil { if err != nil {

View File

@@ -1,50 +0,0 @@
package data
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"
"time"
)
func initPermissions() {
_, err := op.GetPermissionByName("guest")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
pg := &model.Permission{
Name: "guest",
Permission: 0x4000, // 14 bitcan access dir
PathPattern: "*",
AllowOpInfo: []string{"upload", "download", "delete"},
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
if err := op.CreatePermission(pg); err != nil {
panic(err)
} else {
utils.Log.Infof("Successfully created the guest permission ")
}
}
}
_, err = op.GetPermissionByName("admin")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
pa := &model.Permission{
Name: "admin",
Permission: 0x70FF, // 0、1、2、3、4、5、6、7、12、13、14 bit
PathPattern: "",
AllowOpInfo: []string{"upload", "download", "delete"},
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
if err := op.CreatePermission(pa); err != nil {
panic(err)
} else {
utils.Log.Infof("Successfully created the admin permission ")
}
}
}
}

View File

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

View File

@@ -155,7 +155,7 @@ func InitialSettings() []model.SettingItem {
([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:) ([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)
(?U)access_token=(.*)&`, (?U)access_token=(.*)&`,
Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE}, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.OcrApi, Value: "https://api.nn.ci/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL}, {Key: conf.OcrApi, Value: "https://api.alistgo.com/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL},
{Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL}, {Key: conf.FilenameCharMapping, Value: `{"/": "|"}`, Type: conf.TypeText, Group: model.GLOBAL},
{Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL}, {Key: conf.ForwardDirectLinkParams, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL},
{Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, Group: model.GLOBAL}, {Key: conf.IgnoreDirectLinkParams, Value: "sign,alist_ts", Type: conf.TypeString, Group: model.GLOBAL},

View File

@@ -1,10 +1,10 @@
package data package data
import ( import (
"github.com/alist-org/alist/v3/internal/db"
"os" "os"
"github.com/alist-org/alist/v3/cmd/flags" "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/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
@@ -14,47 +14,16 @@ import (
) )
func initUser() { 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: []int{model.ADMIN},
RoleInfo: []uint{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() guest, err := op.GetGuest()
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16) salt := random.String(16)
guestRole, _ := op.GetRoleByName("guest")
guest = &model.User{ guest = &model.User{
Username: "guest", Username: "guest",
PwdHash: model.TwoHashPwd("guest", salt), PwdHash: model.TwoHashPwd("guest", salt),
Salt: salt, Salt: salt,
//Role: []int{model.GUEST}, Role: model.Roles{int(guestRole.ID)},
RoleInfo: []uint{model.GUEST},
BasePath: "/", BasePath: "/",
Permission: 0, Permission: 0,
Disabled: true, Disabled: true,
@@ -67,4 +36,35 @@ func initUser() {
utils.Log.Fatalf("[init user] Failed to get guest user: %v", err) 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

@@ -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_24_0"
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_32_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_41_0"
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
) )
type VersionPatches struct { type VersionPatches struct {
@@ -32,4 +33,10 @@ var UpgradePatches = []VersionPatches{
v3_41_0.GrantAdminPermissions, 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

@@ -12,8 +12,7 @@ var db *gorm.DB
func Init(d *gorm.DB) { func Init(d *gorm.DB) {
db = d 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))
new(model.Permission), new(model.Role))
if err != nil { if err != nil {
log.Fatalf("failed migrate database: %s", err.Error()) 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,56 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"gorm.io/gorm"
"time"
)
// GetLabelIds Get all label_ids from database order by file_name
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
labelFileBinDingDB := db.Model(&model.LabelFileBinDing{})
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
}

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
}

View File

@@ -1,53 +0,0 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
)
func CreatePermission(p *model.Permission) error {
return errors.WithStack(db.Create(p).Error)
}
func UpdatePermission(p *model.Permission) error {
return errors.WithStack(db.Save(p).Error)
}
func GetPermissions(pageIndex, pageSize int) (permissions []model.Permission, count int64, err error) {
permissionDB := db.Model(&model.Permission{})
if err := permissionDB.Count(&count).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get permissions count")
}
if err := permissionDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&permissions).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get find permissions")
}
return permissions, count, nil
}
func DeletePermissionById(id uint) error {
return errors.WithStack(db.Delete(&model.Permission{}, id).Error)
}
func GetPermissionById(id uint) (*model.Permission, error) {
var p model.Permission
if err := db.First(&p, id).Error; err != nil {
return nil, errors.Wrapf(err, "failed get permission by id %d", id)
}
return &p, nil
}
func GetPermissionByIds(ids []uint) ([]model.Permission, error) {
var p []model.Permission
if err := db.Where("id in (?)", ids).First(&p).Error; err != nil {
return nil, errors.Wrapf(err, "failed get permission by ids %v", ids)
}
return p, nil
}
func GetPermissionByName(name string) (*model.Permission, error) {
var p model.Permission
if err := db.Where("name = ?", name).First(&p).Error; err != nil {
return nil, errors.Wrapf(err, "failed get permission by name %v", name)
}
return &p, nil
}

View File

@@ -3,8 +3,37 @@ package db
import ( import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors" "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 { func CreateRole(r *model.Role) error {
return errors.WithStack(db.Create(r).Error) return errors.WithStack(db.Create(r).Error)
} }
@@ -13,41 +42,38 @@ func UpdateRole(r *model.Role) error {
return errors.WithStack(db.Save(r).Error) return errors.WithStack(db.Save(r).Error)
} }
func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err error) { func DeleteRole(id uint) 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 DeleteRoleById(id uint) error {
return errors.WithStack(db.Delete(&model.Role{}, id).Error) return errors.WithStack(db.Delete(&model.Role{}, id).Error)
} }
func GetRoleById(id uint) (*model.Role, error) { func UpdateRolePermissionsPathPrefix(oldPath, newPath string) ([]uint, error) {
var r model.Role var roles []model.Role
if err := db.First(&r, id).Error; err != nil { var modifiedRoleIDs []uint
return nil, errors.Wrapf(err, "failed get role")
}
return &r, nil
}
func GetRoleByIds(ids []uint) ([]model.Role, error) { if err := db.Find(&roles).Error; err != nil {
var r []model.Role return nil, errors.WithMessage(err, "failed to load roles")
if err := db.Where("id in (?)", ids).Find(&r).Error; err != nil {
return nil, errors.Wrapf(err, "failed get roles by ids: %v", ids)
} }
return r, nil
}
func GetRoleByName(name string) (*model.Role, error) { for _, role := range roles {
var r model.Role updated := false
if err := db.Where("name = ?", name).First(&r).Error; err != nil { for i, entry := range role.PermissionScopes {
return nil, errors.Wrapf(err, "failed get role by name %v", name) 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 &r, nil return modifiedRoleIDs, nil
} }

View File

@@ -2,26 +2,26 @@ package db
import ( import (
"encoding/base64" "encoding/base64"
"gorm.io/datatypes"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors" "github.com/pkg/errors"
"gorm.io/gorm"
"path"
"strings"
) )
func GetUserByRole(role []int) (*model.User, error) { func GetUserByRole(role int) (*model.User, error) {
var user model.User var users []model.User
/*user := model.User{Role: role} if err := db.Find(&users).Error; err != nil {
if err := db.Where(user).Take(&user).Error; err != nil {
return nil, err
}*/
cond := datatypes.JSONArrayQuery("role").Contains(role)
err := db.Where(cond).Take(&user).Error
if err != nil {
return nil, err 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 GetUserByName(username string) (*model.User, error) { func GetUserByName(username string) (*model.User, error) {
@@ -107,3 +107,36 @@ func RemoveAuthn(u *model.User, id string) error {
} }
return UpdateAuthn(u.ID, string(res)) return UpdateAuthn(u.ID, string(res))
} }
func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
var users []model.User
var modifiedUsernames []string
if err := db.Find(&users).Error; err != nil {
return nil, errors.WithMessage(err, "failed to load users")
}
oldPathClean := path.Clean(oldPath)
for _, user := range users {
basePath := path.Clean(user.BasePath)
updated := false
if basePath == oldPathClean {
user.BasePath = newPath
updated = true
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
user.BasePath = 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
}

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

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

View File

@@ -6,6 +6,7 @@ import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" 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 { func whetherHide(user *model.User, meta *model.Meta, path string) bool {
// if is admin, don't hide // if user is nil, don't hide
if user == nil || user.CanSeeHides() { 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 return false
} }
// if meta is nil, don't hide // 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

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

View File

@@ -1,128 +0,0 @@
package model
import (
"encoding/json"
"gorm.io/datatypes"
"gorm.io/gorm"
"time"
)
type Permission struct {
ID uint `json:"id" gorm:"primaryKey"` //权限唯一主键
Name string `json:"name"` //权限名称
// Determine permissions by bit
// 0: can see hidden files
// 1: can access without password
// 2: can add offline download tasks
// 3: can mkdir and upload
// 4: can rename
// 5: can move
// 6: can copy
// 7: can remove
// 8: webdav read
// 9: webdav write
// 10: ftp/sftp login and read
// 11: ftp/sftp write
// 12: can read archives
// 13: can decompress archives
// 14: dir access control
Permission int32 `json:"permission"`
PathPattern string `json:"path_pattern"` // 目录路径模式
AllowOp datatypes.JSON `gorm:"type:json;column:allow_op" json:"allow_op"` //允许的操作upload/download/delete等
AllowOpInfo AllowOpSlice `gorm:"-" json:"allow_op_info"`
CreateTime time.Time `json:"create_time"` //创建时间
UpdateTime time.Time `json:"update_time"` //修改时间
}
type AllowOpSlice []string
func (p *Permission) BeforeCreate(db *gorm.DB) (err error) {
if p.AllowOpInfo != nil {
p.AllowOp, err = json.Marshal(p.AllowOpInfo)
if err != nil {
return
}
}
return nil
}
func (p *Permission) BeforeUpdate(db *gorm.DB) (err error) {
if p.AllowOpInfo != nil {
p.AllowOp, err = json.Marshal(p.AllowOpInfo)
if err != nil {
return
}
}
return nil
}
func (p *Permission) AfterFind(db *gorm.DB) (err error) {
p.AllowOpInfo = AllowOpSlice{}
if len(p.AllowOp) > 0 {
err = json.Unmarshal(p.AllowOp, &p.AllowOpInfo)
if err != nil {
return
}
}
return nil
}
func (p *Permission) CanSeeHides() bool {
return p.Permission&1 == 1
}
func (p *Permission) CanAccessWithoutPassword() bool {
return (p.Permission>>1)&1 == 1
}
func (p *Permission) CanAddOfflineDownloadTasks() bool {
return (p.Permission>>2)&1 == 1
}
func (p *Permission) CanWrite() bool {
return (p.Permission>>3)&1 == 1
}
func (p *Permission) CanRename() bool {
return (p.Permission>>4)&1 == 1
}
func (p *Permission) CanMove() bool {
return (p.Permission>>5)&1 == 1
}
func (p *Permission) CanCopy() bool {
return (p.Permission>>6)&1 == 1
}
func (p *Permission) CanRemove() bool {
return (p.Permission>>7)&1 == 1
}
func (p *Permission) CanWebdavRead() bool {
return (p.Permission>>8)&1 == 1
}
func (p *Permission) CanWebdavManage() bool {
return (p.Permission>>9)&1 == 1
}
func (p *Permission) CanFTPAccess() bool {
return (p.Permission>>10)&1 == 1
}
func (p *Permission) CanFTPManage() bool {
return (p.Permission>>11)&1 == 1
}
func (p *Permission) CanReadArchives() bool {
return (p.Permission>>12)&1 == 1
}
func (p *Permission) CanDecompress() bool {
return (p.Permission>>13)&1 == 1
}
func (p *Permission) CanAccessDir() bool {
return (p.Permission>>14)&1 == 1
}

View File

@@ -2,48 +2,51 @@ package model
import ( import (
"encoding/json" "encoding/json"
"gorm.io/datatypes"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
// 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 { type Role struct {
ID uint `json:"id" gorm:"primaryKey"` //角色唯一主键 ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"` //角色名称 Name string `json:"name" gorm:"unique" binding:"required"`
Permissions datatypes.JSON `gorm:"type:json;column:permissions" json:"permissions"` //权限id Description string `json:"description"`
PermissionInfo PermissionIdSlice `gorm:"-" json:"permission_info"` // PermissionScopes stores structured permission list and is ignored by gorm.
CreateTime time.Time `json:"create_time"` //创建时间 PermissionScopes []PermissionEntry `json:"permission_scopes" gorm:"-"`
UpdateTime time.Time `json:"update_time"` //修改时间 // RawPermission is the JSON representation of PermissionScopes stored in DB.
RawPermission string `json:"-" gorm:"type:text"`
} }
type PermissionIdSlice []uint
func (r *Role) BeforeCreate(db *gorm.DB) (err error) { // BeforeSave GORM hook serializes PermissionScopes into RawPermission.
if r.PermissionInfo != nil { func (r *Role) BeforeSave(tx *gorm.DB) error {
r.Permissions, err = json.Marshal(r.PermissionInfo) if len(r.PermissionScopes) == 0 {
if err != nil { r.RawPermission = ""
return return nil
}
} }
bs, err := json.Marshal(r.PermissionScopes)
if err != nil {
return err
}
r.RawPermission = string(bs)
return nil return nil
} }
func (r *Role) BeforeUpdate(db *gorm.DB) (err error) { // AfterFind GORM hook deserializes RawPermission into PermissionScopes.
if r.PermissionInfo != nil { func (r *Role) AfterFind(tx *gorm.DB) error {
r.Permissions, err = json.Marshal(r.PermissionInfo) if r.RawPermission == "" {
if err != nil { r.PermissionScopes = nil
return return nil
}
}
return nil
}
func (r *Role) AfterFind(db *gorm.DB) (err error) {
r.PermissionInfo = PermissionIdSlice{}
if len(r.Permissions) > 0 {
err = json.Unmarshal(r.Permissions, &r.PermissionInfo)
if err != nil {
return
}
} }
var scopes []PermissionEntry
if err := json.Unmarshal([]byte(r.RawPermission), &scopes); err != nil {
return err
}
r.PermissionScopes = scopes
return nil 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

@@ -4,8 +4,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gorm.io/datatypes"
"gorm.io/gorm"
"time" "time"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
@@ -19,21 +17,22 @@ const (
GENERAL = iota GENERAL = iota
GUEST // only one exists GUEST // only one exists
ADMIN ADMIN
NEWGENERAL
) )
const StaticHashSalt = "https://github.com/alist-org/alist" const StaticHashSalt = "https://github.com/alist-org/alist"
type User struct { type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username Username string `json:"username" gorm:"unique" binding:"required"` // username
PwdHash string `json:"-"` // password hash PwdHash string `json:"-"` // password hash
PwdTS int64 `json:"-"` // password timestamp PwdTS int64 `json:"-"` // password timestamp
Salt string `json:"-"` // unique salt Salt string `json:"-"` // unique salt
Password string `json:"password"` // password Password string `json:"password"` // password
BasePath string `json:"base_path"` // base path BasePath string `json:"base_path"` // base path
Role datatypes.JSON `gorm:"type:json;column:role" json:"role"` // user's role Role Roles `json:"role" gorm:"type:text"` // user's roles
RoleInfo RoleIdSlice `gorm:"-" json:"role_info"` RolesDetail []Role `json:"-" gorm:"-"`
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
// Determine permissions by bit // Determine permissions by bit
// 0: can see hidden files // 0: can see hidden files
// 1: can access without password // 1: can access without password
@@ -49,66 +48,19 @@ type User struct {
// 11: ftp/sftp write // 11: ftp/sftp write
// 12: can read archives // 12: can read archives
// 13: can decompress archives // 13: can decompress archives
// 14: check path limit
Permission int32 `json:"permission"` Permission int32 `json:"permission"`
OtpSecret string `json:"-"` OtpSecret string `json:"-"`
SsoID string `json:"sso_id"` // unique by sso platform SsoID string `json:"sso_id"` // unique by sso platform
Authn string `gorm:"type:text" json:"-"` Authn string `gorm:"type:text" json:"-"`
} }
type RoleIdSlice []uint
func (u *User) BeforeCreate(db *gorm.DB) (err error) {
if u.RoleInfo != nil {
u.Role, err = json.Marshal(u.RoleInfo)
if err != nil {
return
}
}
return nil
}
func (u *User) BeforeUpdate(db *gorm.DB) (err error) {
if u.RoleInfo != nil {
u.Role, err = json.Marshal(u.RoleInfo)
if err != nil {
return
}
}
return nil
}
func (u *User) AfterFind(db *gorm.DB) (err error) {
u.RoleInfo = RoleIdSlice{}
if len(u.Role) > 0 {
err = json.Unmarshal(u.Role, &u.RoleInfo)
if err != nil {
return
}
}
return nil
}
func (u *User) IsGuest() bool { func (u *User) IsGuest() bool {
isGuest := true return u.Role.Contains(GUEST)
for _, role := range u.Role {
if role != GUEST {
isGuest = false
break
}
}
return isGuest
//return u.Role == GUEST
} }
func (u *User) IsAdmin() bool { func (u *User) IsAdmin() bool {
isAdmin := true return u.Role.Contains(ADMIN)
for _, role := range u.Role {
if role != ADMIN {
isAdmin = false
}
}
return isAdmin
//return u.Role == ADMIN
} }
func (u *User) ValidateRawPassword(password string) error { func (u *User) ValidateRawPassword(password string) error {
@@ -188,8 +140,19 @@ func (u *User) CanDecompress() bool {
return (u.Permission>>13)&1 == 1 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) { func (u *User) JoinPath(reqPath string) (string, error) {
return utils.JoinBasePath(u.BasePath, reqPath) path, err := utils.JoinBasePath(u.BasePath, reqPath)
if err != nil {
return "", err
}
if u.CheckPathLimit() && !utils.IsSubPath(u.BasePath, path) {
return "", errs.PermissionDenied
}
return path, nil
} }
func StaticHash(password string) string { func StaticHash(password string) string {
@@ -228,5 +191,5 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
} }
func (u *User) WebAuthnIcon() string { func (u *User) WebAuthnIcon() string {
return "https://alist.nn.ci/logo.svg" return "https://alistgo.com/logo.svg"
} }

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,159 @@
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"`
}
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 CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
}
if req.LabelIds == "" {
return nil
}
labelMap := strings.Split(req.LabelIds, ",")
for _, value := range labelMap {
labelId, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid label ID '%s': %v", value, err)
}
if err = db.CreateLabelFileBinDing(req.Name, uint(labelId), userId); err != nil {
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,
}
err := db.CreateObjFile(objFile)
if 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
}

View File

@@ -1,30 +0,0 @@
package op
import (
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
)
func CreatePermission(p *model.Permission) error {
return db.CreatePermission(p)
}
func GetPermissions(pageIndex, pageSize int) (permissions []model.Permission, count int64, err error) {
return db.GetPermissions(pageIndex, pageSize)
}
func GetPermissionById(id uint) (*model.Permission, error) {
return db.GetPermissionById(id)
}
func UpdatePermission(p *model.Permission) error {
return db.UpdatePermission(p)
}
func DeletePermissionById(id uint) error {
return db.DeletePermissionById(id)
}
func GetPermissionByName(name string) (*model.Permission, error) {
return db.GetPermissionByName(name)
}

View File

@@ -1,50 +1,121 @@
package op package op
import ( import (
"fmt"
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/db" "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/internal/model"
"github.com/alist-org/alist/v3/pkg/singleflight"
"github.com/alist-org/alist/v3/pkg/utils"
) )
func CreateRole(r *model.Role) error { var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
return db.CreateRole(r) var roleG singleflight.Group[*model.Role]
}
func GetRoles(pageIndex, pageSize int) (roles []model.Role, count int64, err error) { func GetRole(id uint) (*model.Role, error) {
return db.GetRoles(pageIndex, pageSize) key := fmt.Sprint(id)
} if r, ok := roleCache.Get(key); ok {
return r, nil
func GetRoleById(id uint) (*model.Role, error) {
return db.GetRoleById(id)
}
func GetRoleByIds(ids []uint) ([]model.Role, error) {
return db.GetRoleByIds(ids)
}
func UpdateRole(r *model.Role) error {
return db.UpdateRole(r)
}
func DeleteRoleById(id uint) error {
return db.DeleteRoleById(id)
}
func GetPermissionByRoleIds(ids []uint) ([]model.Permission, error) {
roles, err := db.GetRoleByIds(ids)
if err != nil {
return nil, err
} }
perIds := make([]uint, 0) r, err, _ := roleG.Do(key, func() (*model.Role, error) {
for _, v := range roles { _r, err := db.GetRole(id)
perIds = append(perIds, v.PermissionInfo...) if err != nil {
} return nil, err
permissions, err := db.GetPermissionByIds(perIds) }
if err != nil { roleCache.Set(key, _r, cache.WithEx[*model.Role](time.Hour))
return nil, err return _r, nil
} })
return permissions, nil return r, err
} }
func GetRoleByName(name string) (*model.Role, error) { func GetRoleByName(name string) (*model.Role, error) {
return db.GetRoleByName(name) 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
}
if old.Name == "admin" || old.Name == "guest" {
return errs.ErrChangeDefaultRole
}
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.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

@@ -216,6 +216,21 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
if oldStorage.MountPath != storage.MountPath { if oldStorage.MountPath != storage.MountPath {
// mount path renamed, need to drop the storage // mount path renamed, need to drop the storage
storagesMap.Delete(oldStorage.MountPath) 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 _, name := range modifiedUsernames {
userCache.Del(name)
}
} }
if err != nil { if err != nil {
return errors.WithMessage(err, "failed get storage driver") return errors.WithMessage(err, "failed get storage driver")

View File

@@ -18,7 +18,11 @@ var adminUser *model.User
func GetAdmin() (*model.User, error) { func GetAdmin() (*model.User, error) {
if adminUser == nil { if adminUser == nil {
user, err := db.GetUserByRole([]int{model.ADMIN}) role, err := GetRoleByName("admin")
if err != nil {
return nil, err
}
user, err := db.GetUserByRole(int(role.ID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -29,7 +33,11 @@ func GetAdmin() (*model.User, error) {
func GetGuest() (*model.User, error) { func GetGuest() (*model.User, error) {
if guestUser == nil { if guestUser == nil {
user, err := db.GetUserByRole([]int{model.GUEST}) role, err := GetRoleByName("guest")
if err != nil {
return nil, err
}
user, err := db.GetUserByRole(int(role.ID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -38,7 +46,7 @@ func GetGuest() (*model.User, error) {
return guestUser, nil return guestUser, nil
} }
func GetUserByRole(role []int) (*model.User, error) { func GetUserByRole(role int) (*model.User, error) {
return db.GetUserByRole(role) return db.GetUserByRole(role)
} }

View File

@@ -1,15 +1,11 @@
package common package common
import ( import (
"path"
"strings"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/dlclark/regexp2"
) )
func IsStorageSignEnabled(rawPath string) bool { 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 { 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 // Deprecated: CanAccess is kept for backward compatibility.
if meta != nil && !user.CanSeeHides() && meta.Hide != "" && // The logic has been moved to CanAccessWithRoles which performs the
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path // necessary checks based on role permissions. This wrapper ensures
for _, hide := range strings.Split(meta.Hide, "\n") { // older calls still work without relying on user permission bits.
re := regexp2.MustCompile(hide, regexp2.None) return CanAccessWithRoles(user, meta, reqPath, password)
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
} }
// ShouldProxy TODO need optimize // ShouldProxy TODO need optimize

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

@@ -0,0 +1,108 @@
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
}
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
}
for _, rid := range u.Role {
role, err := op.GetRole(uint(rid))
if err != nil {
continue
}
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) {
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/op"
"github.com/alist-org/alist/v3/internal/setting" "github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils" "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/ftp"
"math/rand" "math/rand"
"net" "net"
@@ -130,7 +131,8 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
return nil, err 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") 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 { if err != nil {
return err 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)) meta, err := op.GetNearestMeta(stdpath.Dir(reqPath))
if err != nil { if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) { 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 { func Remove(ctx context.Context, path string) error {
user := ctx.Value("user").(*model.User) 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 return errs.PermissionDenied
} }
reqPath, err := user.JoinPath(path) reqPath, err := user.JoinPath(path)
@@ -56,13 +58,14 @@ func Rename(ctx context.Context, oldPath, newPath string) error {
} }
srcDir, srcBase := stdpath.Split(srcPath) srcDir, srcBase := stdpath.Split(srcPath)
dstDir, dstBase := stdpath.Split(dstPath) dstDir, dstBase := stdpath.Split(dstPath)
permSrc := common.MergeRolePermissions(user, srcPath)
if srcDir == dstDir { if srcDir == dstDir {
if !user.CanRename() || !user.CanFTPManage() { if !common.HasPermission(permSrc, common.PermRename) || !common.HasPermission(permSrc, common.PermFTPManage) {
return errs.PermissionDenied return errs.PermissionDenied
} }
return fs.Rename(ctx, srcPath, dstBase) return fs.Rename(ctx, srcPath, dstBase)
} else { } 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 return errs.PermissionDenied
} }
if err = fs.Move(ctx, srcPath, dstDir); err != nil { 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) 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 return nil, errs.PermissionDenied
} }
@@ -125,7 +125,7 @@ func Stat(ctx context.Context, path string) (os.FileInfo, error) {
} }
} }
ctx = context.WithValue(ctx, "meta", meta) 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 return nil, errs.PermissionDenied
} }
obj, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) 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) 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 return nil, errs.PermissionDenied
} }
objs, err := fs.List(ctx, reqPath, &fs.ListArgs{}) objs, err := fs.List(ctx, reqPath, &fs.ListArgs{})

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"image/png" "image/png"
"path"
"strings"
"time" "time"
"github.com/Xhofe/go-cache" "github.com/Xhofe/go-cache"
@@ -89,16 +91,16 @@ func loginHash(c *gin.Context, req *LoginReq) {
type UserResp struct { type UserResp struct {
model.User model.User
Otp bool `json:"otp"` Otp bool `json:"otp"`
Permission int32 `json:"permission"` RoleNames []string `json:"role_names"`
PathPattern []string `json:"path_pattern"` // 目录路径模式当Permission第14bit位为1时用到 Permissions []model.PermissionEntry `json:"permissions"`
AllowOpInfo model.AllowOpSlice `json:"allow_op_info"`
} }
// CurrentUser get current user by token // CurrentUser get current user by token
// if token is empty, return guest user // if token is empty, return guest user
func CurrentUser(c *gin.Context) { func CurrentUser(c *gin.Context) {
user := c.MustGet("user").(*model.User) user := c.MustGet("user").(*model.User)
userResp := UserResp{ userResp := UserResp{
User: *user, User: *user,
} }
@@ -106,39 +108,31 @@ func CurrentUser(c *gin.Context) {
if userResp.OtpSecret != "" { if userResp.OtpSecret != "" {
userResp.Otp = true userResp.Otp = true
} }
permissions, err := op.GetPermissionByRoleIds(user.RoleInfo)
if err != nil || len(permissions) == 0 {
common.ErrorResp(c, err, 400)
}
if len(permissions) == 1 {
userResp.Permission = permissions[0].Permission
userResp.PathPattern = append(userResp.PathPattern, permissions[0].PathPattern)
userResp.AllowOpInfo = permissions[0].AllowOpInfo
} else {
var per int32
for _, perm := range permissions {
per |= perm.Permission
userResp.PathPattern = append(userResp.PathPattern, perm.PathPattern)
userResp.AllowOpInfo = append(userResp.AllowOpInfo, perm.AllowOpInfo...)
}
userResp.PathPattern = uniqStr(userResp.PathPattern)
userResp.AllowOpInfo = uniqStr(userResp.AllowOpInfo)
userResp.Permission = per
}
common.SuccessResp(c, userResp)
}
func uniqStr(str []string) []string { var roleNames []string
seen := make(map[string]int) permMap := map[string]int32{}
j := 0 addedPaths := map[string]bool{}
for i, v := range str {
if _, ok := seen[v]; !ok { for _, role := range user.RolesDetail {
str[j] = str[i] roleNames = append(roleNames, role.Name)
seen[v] = j for _, entry := range role.PermissionScopes {
j++ cleanPath := path.Clean("/" + strings.TrimPrefix(entry.Path, "/"))
permMap[cleanPath] |= entry.Permission
} }
} }
return str[:j] 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)
} }
func UpdateCurrent(c *gin.Context) { func UpdateCurrent(c *gin.Context) {

View File

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

View File

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

View File

@@ -48,12 +48,28 @@ type ObjResp struct {
} }
type FsListResp struct { type FsListResp struct {
Content []ObjResp `json:"content"` Content []ObjLabelResp `json:"content"`
Total int64 `json:"total"` Total int64 `json:"total"`
Readme string `json:"readme"` Readme string `json:"readme"`
Header string `json:"header"` Header string `json:"header"`
Write bool `json:"write"` Write bool `json:"write"`
Provider string `json:"provider"` 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) { func FsList(c *gin.Context) {
@@ -77,11 +93,12 @@ func FsList(c *gin.Context) {
} }
} }
c.Set("meta", meta) 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) common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return 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) common.ErrorStrResp(c, "Refresh without permission", 403)
return return
} }
@@ -97,11 +114,11 @@ func FsList(c *gin.Context) {
provider = storage.GetStorage().Driver provider = storage.GetStorage().Driver
} }
common.SuccessResp(c, FsListResp{ common.SuccessResp(c, FsListResp{
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)), Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath), user.ID),
Total: int64(total), Total: int64(total),
Readme: getReadme(meta, reqPath), Readme: getReadme(meta, reqPath),
Header: getHeader(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, Provider: provider,
}) })
} }
@@ -135,7 +152,7 @@ func FsDirs(c *gin.Context) {
} }
} }
c.Set("meta", meta) 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) common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return return
} }
@@ -207,11 +224,15 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
return total, objs[start:end] return total, objs[start:end]
} }
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp { func toObjsResp(objs []model.Obj, parent string, encrypt bool, userId uint) []ObjLabelResp {
var resp []ObjResp var resp []ObjLabelResp
for _, obj := range objs { for _, obj := range objs {
var labels []model.Label
if obj.IsDir() == false {
labels, _ = op.GetLabelByFileName(userId, obj.GetName())
}
thumb, _ := model.GetThumb(obj) thumb, _ := model.GetThumb(obj)
resp = append(resp, ObjResp{ resp = append(resp, ObjLabelResp{
Id: obj.GetID(), Id: obj.GetID(),
Path: obj.GetPath(), Path: obj.GetPath(),
Name: obj.GetName(), Name: obj.GetName(),
@@ -224,6 +245,7 @@ func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
Sign: common.Sign(obj, parent, encrypt), Sign: common.Sign(obj, parent, encrypt),
Thumb: thumb, Thumb: thumb,
Type: utils.GetObjType(obj.GetName(), obj.IsDir()), Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
LabelList: labels,
}) })
} }
return resp return resp
@@ -236,11 +258,11 @@ type FsGetReq struct {
type FsGetResp struct { type FsGetResp struct {
ObjResp ObjResp
RawURL string `json:"raw_url"` RawURL string `json:"raw_url"`
Readme string `json:"readme"` Readme string `json:"readme"`
Header string `json:"header"` Header string `json:"header"`
Provider string `json:"provider"` Provider string `json:"provider"`
Related []ObjResp `json:"related"` Related []ObjLabelResp `json:"related"`
} }
func FsGet(c *gin.Context) { func FsGet(c *gin.Context) {
@@ -263,7 +285,7 @@ func FsGet(c *gin.Context) {
} }
} }
c.Set("meta", meta) 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) common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return return
} }
@@ -347,7 +369,7 @@ func FsGet(c *gin.Context) {
Readme: getReadme(meta, reqPath), Readme: getReadme(meta, reqPath),
Header: getHeader(meta, reqPath), Header: getHeader(meta, reqPath),
Provider: provider, Provider: provider,
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)), Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath), user.ID),
}) })
} }
@@ -391,7 +413,7 @@ func FsOther(c *gin.Context) {
} }
} }
c.Set("meta", meta) 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) common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
return 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,103 @@
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"
"strconv"
)
type DelLabelFileBinDingReq struct {
FileName string `json:"file_name"`
LabelId string `json:"label_id"`
}
func GetLabelByFileName(c *gin.Context) {
fileName := c.Query("file_name")
if fileName == "" {
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
}
labels, err := op.GetLabelByFileName(userObj.ID, fileName)
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)
}

View File

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

View File

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

View File

@@ -1,69 +0,0 @@
package handles
import (
"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 CreatePermission(c *gin.Context) {
var req model.Permission
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.CreatePermission(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}
func ListPermissions(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)
permissions, total, err := op.GetPermissions(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, common.PageResp{
Content: permissions,
Total: total,
})
}
func UpdatePermission(c *gin.Context) {
var req model.Permission
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.UpdatePermission(&req); err != nil {
common.ErrorResp(c, err, 500)
} else {
common.SuccessResp(c)
}
}
func DeletePermission(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.DeletePermissionById(uint(id)); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}

View File

@@ -1,14 +1,47 @@
package handles package handles
import ( 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/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"strconv"
) )
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) { func CreateRole(c *gin.Context) {
var req model.Role var req model.Role
if err := c.ShouldBind(&req); err != nil { if err := c.ShouldBind(&req); err != nil {
@@ -22,33 +55,23 @@ func CreateRole(c *gin.Context) {
} }
} }
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)
permissions, total, err := op.GetRoles(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, common.PageResp{
Content: permissions,
Total: total,
})
}
func UpdateRole(c *gin.Context) { func UpdateRole(c *gin.Context) {
var req model.Role var req model.Role
if err := c.ShouldBind(&req); err != nil { if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400) common.ErrorResp(c, err, 400)
return return
} }
role, err := op.GetRole(req.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.UpdateRole(&req); err != nil { if err := op.UpdateRole(&req); err != nil {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500, true)
} else { } else {
common.SuccessResp(c) common.SuccessResp(c)
} }
@@ -61,27 +84,18 @@ func DeleteRole(c *gin.Context) {
common.ErrorResp(c, err, 400) common.ErrorResp(c, err, 400)
return return
} }
if err := op.DeleteRoleById(uint(id)); err != nil { role, err := op.GetRole(uint(id))
common.ErrorResp(c, err, 500) 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 return
} }
common.SuccessResp(c) common.SuccessResp(c)
} }
type GetPermissionByRoleIdsReq struct {
Ids []uint `json:"ids"`
}
func GetPermissionByRoleIds(c *gin.Context) {
var req GetPermissionByRoleIdsReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
permissions, err := op.GetPermissionByRoleIds(req.Ids)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c, permissions)
}

View File

@@ -57,7 +57,7 @@ func Search(c *gin.Context) {
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
continue 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 continue
} }
filteredNodes = append(filteredNodes, node) filteredNodes = append(filteredNodes, node)

View File

@@ -154,10 +154,9 @@ func autoRegister(username, userID string, err error) (*model.User, error) {
Password: random.String(16), Password: random.String(16),
Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)), Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),
BasePath: setting.GetStr(conf.SSODefaultDir), BasePath: setting.GetStr(conf.SSODefaultDir),
//Role: []int{0}, Role: nil,
RoleInfo: []uint{0}, Disabled: false,
Disabled: false, SsoID: userID,
SsoID: userID,
} }
if err = db.CreateUser(user); err != nil { if err = db.CreateUser(user); err != nil {
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") && strings.HasSuffix(err.Error(), "username") { if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") && strings.HasSuffix(err.Error(), "username") {

View File

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

View File

@@ -60,10 +60,10 @@ func UpdateUser(c *gin.Context) {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return return
} }
/*if !reflect.DeepEqual(user.Role, req.Role) { //if !utils.SliceEqual(user.Role, req.Role) {
common.ErrorStrResp(c, "role can not be changed", 400) // common.ErrorStrResp(c, "role can not be changed", 400)
return // return
}*/ //}
if req.Password == "" { if req.Password == "" {
req.PwdHash = user.PwdHash req.PwdHash = user.PwdHash
req.Salt = user.Salt req.Salt = user.Salt

View File

@@ -2,6 +2,7 @@ package middlewares
import ( import (
"crypto/subtle" "crypto/subtle"
"fmt"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
@@ -68,6 +69,15 @@ func Auth(c *gin.Context) {
c.Abort() c.Abort()
return 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) c.Set("user", user)
log.Debugf("use login token: %+v", user) log.Debugf("use login token: %+v", user)
c.Next() c.Next()
@@ -122,6 +132,19 @@ func Authn(c *gin.Context) {
c.Abort() c.Abort()
return 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) c.Set("user", user)
log.Debugf("use login token: %+v", user) log.Debugf("use login token: %+v", user)
c.Next() c.Next()

View File

@@ -35,7 +35,9 @@ func FsUp(c *gin.Context) {
return 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) common.ErrorResp(c, errs.PermissionDenied, 403)
c.Abort() c.Abort()
return return

View File

@@ -120,6 +120,13 @@ func admin(g *gin.RouterGroup) {
user.GET("/sshkey/list", handles.ListPublicKeys) user.GET("/sshkey/list", handles.ListPublicKeys)
user.POST("/sshkey/delete", handles.DeletePublicKey) 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 := g.Group("/storage")
storage.GET("/list", handles.ListStorages) storage.GET("/list", handles.ListStorages)
storage.GET("/get", handles.GetStorage) storage.GET("/get", handles.GetStorage)
@@ -162,19 +169,18 @@ func admin(g *gin.RouterGroup) {
index.POST("/clear", middlewares.SearchIndex, handles.ClearIndex) index.POST("/clear", middlewares.SearchIndex, handles.ClearIndex)
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress) index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
permission := g.Group("/permission") label := g.Group("/label")
permission.POST("/create", handles.CreatePermission) label.GET("/list", handles.ListLabel)
permission.GET("/list", handles.ListPermissions) label.GET("/get", handles.GetLabel)
permission.POST("/update", handles.UpdatePermission) label.POST("/create", handles.CreateLabel)
permission.POST("/delete", handles.DeletePermission) label.POST("/update", handles.UpdateLabel)
label.POST("/delete", handles.DeleteLabel)
role := g.Group("/role")
role.POST("/create", handles.CreateRole)
role.GET("/list", handles.ListRoles)
role.POST("/update", handles.UpdateRole)
role.POST("/delete", handles.DeleteRole)
role.GET("/get_permission", handles.GetPermissionByRoleIds)
labelFileBinding := g.Group("/label_file_binding")
labelFileBinding.GET("/get", handles.GetLabelByFileName)
labelFileBinding.GET("/get_file_by_label", handles.GetFileByLabel)
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
labelFileBinding.POST("/delete", handles.DelLabelByFileName)
} }
func _fs(g *gin.RouterGroup) { func _fs(g *gin.RouterGroup) {

View File

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

View File

@@ -3,16 +3,19 @@ package server
import ( import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/server/middlewares"
"net/http" "net/http"
"net/url"
"path" "path"
"strings" "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/conf"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting" "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/alist-org/alist/v3/server/webdav"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -92,7 +95,19 @@ func WebDAVAuth(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
if user.Disabled || !user.CanWebdavRead() { 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)
if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) {
if c.Request.Method == "OPTIONS" { if c.Request.Method == "OPTIONS" {
c.Set("user", guest) c.Set("user", guest)
c.Next() c.Next()
@@ -102,27 +117,27 @@ func WebDAVAuth(c *gin.Context) {
c.Abort() c.Abort()
return 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.Status(http.StatusForbidden)
c.Abort() c.Abort()
return 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.Status(http.StatusForbidden)
c.Abort() c.Abort()
return 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.Status(http.StatusForbidden)
c.Abort() c.Abort()
return 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.Status(http.StatusForbidden)
c.Abort() c.Abort()
return return
} }
if c.Request.Method == "PROPPATCH" && !user.CanWebdavManage() { if c.Request.Method == "PROPPATCH" && !common.HasPermission(perm, common.PermWebdavManage) {
c.Status(http.StatusForbidden) c.Status(http.StatusForbidden)
c.Abort() c.Abort()
return return

View File

@@ -14,6 +14,7 @@ import (
"github.com/alist-org/alist/v3/internal/fs" "github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op" "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 // 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) srcName := path.Base(src)
dstName := path.Base(dst) dstName := path.Base(dst)
user := ctx.Value("user").(*model.User) 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 return http.StatusForbidden, nil
} }
if srcName != dstName && !user.CanRename() { if srcName != dstName && !common.HasPermission(perm, common.PermRename) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if srcDir == dstDir { if srcDir == dstDir {