Compare commits

..

31 Commits

Author SHA1 Message Date
千石
e1800f18e4 feat: Check usage before deleting storage (#9322)
* feat(storage): Added role and user path checking functionality

- Added `GetAllRoles` function to retrieve all roles
- Added `GetAllUsers` function to retrieve all users
- Added `firstPathSegment` function to extract the first segment of a path
- Checks whether a storage object is used by a role or user, and returns relevant information for unusing it

* fix(storage): Fixed a potential null value issue with not checking firstMount.

- Added a check to see if `firstMount` is null to prevent logic errors.
- Adjusted the loading logic of `GetAllRoles` and `GetAllUsers` to only execute when `firstMount` is non-null.
- Fixed the `usedBy` check logic to ensure that an error message is returned under the correct conditions.
- Optimized code structure to reduce unnecessary execution paths.
2025-09-12 17:56:23 +08:00
D@' 3z K!7
16cce37947 fix(drivers): add session renewal cron for MediaFire driver (#9321)
- Implement automatic session token renewal every 6-9 minutes
- Add validation for required SessionToken and Cookie fields in Init
- Handle session expiration by calling renewToken on validation failure
- Prevent storage failures due to MediaFire session timeouts

Fixes session closure issues that occur after server restarts or extended periods.

Co-authored-by: Da3zKi7 <da3zki7@duck.com>
2025-09-12 17:53:47 +08:00
千石
6e7c7d1dd0 refactor (auth): Optimize permission path processing logic (#9320)
- Changed permission path collection from map to slice to improve code readability
- Removed redundant path checks to improve path addition efficiency
- Restructured the loop logic for path processing to simplify the path permission assignment process
2025-09-11 21:16:33 +08:00
Chesyre
28a8428559 feat(driver): add Gofile storage driver (#9318)
Add support for Gofile.io cloud storage service with full CRUD operations.
Features:
- File and folder listing
- Upload and download functionality
- Create, move, rename, copy, and delete operations
- Direct link generation for file access
- API token authentication
The driver implements all required driver interfaces and follows
the existing driver patterns in the codebase.
2025-09-11 11:46:31 +08:00
D@' 3z K!7
d0026030cb feat(drivers): add MediaFire driver support (#9319)
- Implement complete MediaFire storage driver
- Add authentication via session_token and cookie
- Support all core operations: List, Get, Link, Put, Copy, Move, Remove, Rename, MakeDir
- Include thumbnail generation for media files
- Handle MediaFire's resumable upload API with multi-unit transfers
- Add proper error handling and progress reporting

Closes 请求支持Mediafire #7869

Co-authored-by: Da3zKi7 <da3zki7@duck.com>
2025-09-11 11:46:09 +08:00
千石
fcbc79cb24 feat: Support 123pan safebox (#9311)
* feat(meta): Added a SafePassword field

- Added the SafePassword field to meta.go
- Revised the field format to align with the code style
- The SafePassword field is used to supplement the extended functionality

* feat(driver): Added support for safe unlocking logic

- Added safe file unlocking logic in `driver.go`, returning an error if unlocking fails.
- Introduced the `safeBoxUnlocked` variable of type `sync.Map` to record the IDs of unlocked files.
- Enhanced error handling logic to automatically attempt to unlock safe files and re-retrieve the file list.
- Added the `IsLock` field to file types in `types.go` to identify whether they are safe files.
- Added a constant definition for the `SafeBoxUnlock` interface address in `util.go`.
- Added the `unlockSafeBox` method to unlock a safe with a specified file ID via the API.
- Optimized the file retrieval logic to automatically call the unlock method when the safe is locked.

* Refactor (driver): Optimize lock field type

- Changed the `IsLock` field type from `int` to `bool` for better semantics.
- Updated the check logic to use direct Boolean comparisons to improve code readability and accuracy.
2025-09-05 19:58:27 +08:00
Sakkyoi Cheng
930f9f6096 fix(ssologin): missing role in SSO auto-registration and minor callback issue (#9305)
* fix(ssologin): return after error response

* fix(ssologin): set default role for SSO user creation
2025-09-04 22:15:39 +08:00
千石
23107483a1 Refactor (storage): Comment out the path validation logic (#9308)
- Comment out the error return logic for paths with "/"
- Remove storage path restrictions to allow for flexible handling of root paths
2025-09-04 22:14:33 +08:00
千石
4b288a08ef fix: session invalid issue (#9301)
* feat(auth): Enhanced device login session management

- Upon login, obtain and verify `Client-Id` to ensure unique device sessions.
- If there are too many device sessions, clean up old ones according to the configured policy or return an error.
- If a device session is invalid, deregister the old token and return a 401 error.
- Added `EnsureActiveOnLogin` function to handle the creation and refresh of device sessions during login.

* feat(session): Modified session deletion logic to mark sessions as inactive.

- Changed session deletion logic to mark sessions as inactive using the `MarkInactive` method.
- Adjusted error handling to ensure an error is returned if marking fails.

* feat(session): Added device limits and eviction policies

- Added a device limit, controlling the maximum number of devices using the `MaxDevices` configuration option.
- If the number of devices exceeds the limit, the configured eviction policy is used.
- If the policy is `evict_oldest`, the oldest device is evicted.
- Otherwise, an error message indicating too many devices is returned.

* refactor(session): Filter for the user's oldest active session

- Renamed `GetOldestSession` to `GetOldestActiveSession` to more accurately reflect its functionality
- Updated the SQL query to add the `status = SessionActive` condition to retrieve only active sessions
- Replaced all callpoints and unified the new function name to ensure logical consistency
2025-08-29 21:20:29 +08:00
Sky_slience
63391a2091 fix(readme): remove outdated sponsor links from README files (#9300)
Co-authored-by: Sky_slience <Skyslience@spdzy.com>
2025-08-29 14:56:54 +08:00
JoaHuang
a11e4cfb31 Merge pull request #9299 from okatu-loli/session-manage-2
fix: session login error
2025-08-29 13:45:10 +08:00
okatu-loli
9a7c82a71e feat(auth): Optimized device session handling logic
- Introduced middleware to handle device sessions
- Changed `handleSession` to `HandleSession` in multiple places in `auth.go` to maintain consistent naming
- Updated response structure to return `device_key` and `token`
2025-08-29 13:31:44 +08:00
okatu-loli
8623da5361 feat(session): Added user session limit and device eviction logic
- Renamed `CountSessionsByUser` to `CountActiveSessionsByUser` and added session status filtering
- Added user and device session limit, with policy handling when exceeding the limit
- Introduced device eviction policy: If the maximum number of devices is exceeded, the oldest session will be evicted using the "evict_oldest" policy
- Modified `LastActive` update logic to ensure accurate session activity time
2025-08-29 11:53:55 +08:00
千石
84adba3acc feat(user): Enhanced role assignment logic (#9297)
- Imported the `utils` package
- Modified the role assignment logic to prevent assigning administrator or guest roles to users
2025-08-28 09:57:34 +08:00
千石
3bf0af1e68 fix(session): Fixed the session status update logic. (#9296)
- Removed the error returned when the session status is `SessionInactive`.
- Updated the `LastActive` field of the session to always record the current time.
2025-08-28 09:57:13 +08:00
千石
de09ba08b6 chore(deps): Update 115driver dependency to v1.1.2 (#9294)
- Upgrade `github.com/SheltonZhu/115driver` to v1.1.2 in `go.mod`
- Modify `replace` to point to `github.com/okatu-loli/115driver v1.1.2`
- Remove old version checksum from `go.sum` and add new version checksum
2025-08-27 17:46:34 +08:00
千石
c64f899a63 feat: implement session management (#9286)
* feat(auth): Added device session management

- Added the `handleSession` function to manage user device sessions and verify client identity
- Updated `auth.go` to call `handleSession` for device handling when a user logs in
- Added the `Session` model to database migrations
- Added `device.go` and `session.go` files to handle device session logic
- Updated `settings.go` to add device-related configuration items, such as the maximum number of devices, device eviction policy, and session TTL

* feat(session): Adds session management features

- Added `SessionInactive` error type in `device.go`
- Added session-related APIs in `router.go` to support listing and evicting sessions
- Added `ListSessionsByUser`, `ListSessions`, and `MarkInactive` methods in `session.go`
- Returns an appropriate error when the session state is `SessionInactive`

* feat(auth): Marks the device session as invalid.

- Import the `session` package into the `auth` module to handle device session status.
- Add a check in the login logic. If `device_key` is obtained, call `session.MarkInactive` to mark the device session as invalid.
- Store the invalid status in the context variable `session_inactive` for subsequent middleware checks.
- Add a check in the session refresh logic to abort the process if the current session has been marked invalid.

* feat(auth, session): Added device information processing and session management changes

- Updated device handling logic in `auth.go` to pass user agent and IP information
- Adjusted database queries in `session.go` to optimize session query fields and add `user_agent` and `ip` fields
- Modified the `Handle` method to add `ua` and `ip` parameters to store the user agent and IP address
- Added the `SessionResp` structure to return a session response containing `user_agent` and `ip`
- Updated the `/admin/user/create` and `/webdav` endpoints to pass the user agent and IP address to the device handler
2025-08-25 19:46:38 +08:00
千石
3319f6ea6a feat(search): Optimized search result filtering and paging logic (#9287)
- Introduced the `filteredNodes` list to optimize the node filtering process
- Filtered results based on the page limit during paging
- Modified search logic to ensure nodes are within the user's base path
- Added access permission checks for node metadata
- Adjusted paging logic to avoid redundant node retrieval
2025-08-25 19:46:24 +08:00
千石
d7723c378f chore(deps): Upgrade 115driver to v1.1.1 (#9283)
- Upgraded `github.com/SheltonZhu/115driver` from v1.0.34 to v1.1.1
- Updated the corresponding version verification information in `go.sum`
2025-08-25 19:46:10 +08:00
千石
a9fcd51bc4 fix: ensure DefaultRole stores role ID while exposing role name in APIs (#9279)
* fix(setting): ensure DefaultRole stores role ID while exposing role name in APIs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Delete server/webdav/makepropstatresponse_test.go

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

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

---------

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

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

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

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

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

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

* fix: resolve webdav handshake error in permission checks

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

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

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

* fix: resolve webdav handshake error in permission checks

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

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

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

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

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

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

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

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

* fix(driver): 123 download url error

---------

Co-authored-by: Sky_slience <Skyslience@spdzy.com>
2025-07-30 16:55:32 +08:00
69 changed files with 4546 additions and 173 deletions

View File

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

View File

@@ -57,6 +57,7 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
@@ -121,8 +122,6 @@ https://alistgo.com/guide/sponsor.html
### Special sponsors
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## Contributors

View File

@@ -57,6 +57,7 @@
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] WebDav(支持无API的OneDrive/SharePoint)
- [x] Teambition[中国](https://www.teambition.com/ )[国际](https://us.teambition.com/ )
- [x] [MediaFire](https://www.mediafire.com)
- [x] [分秒帧](https://www.mediatrack.cn/)
- [x] [和彩云](https://yun.139.com/) (个人云, 家庭云,共享群组)
- [x] [Yandex.Disk](https://disk.yandex.com/)
@@ -118,8 +119,6 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## 贡献者

View File

@@ -57,6 +57,7 @@
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] WebDav(Support OneDrive/SharePoint without API)
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn/)
- [x] [139yun](https://yun.139.com/) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com/)
@@ -120,8 +121,6 @@ https://alistgo.com/guide/sponsor.html
### スペシャルスポンサー
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](http://zhaoziyuan2.cc/) - 阿里云盘资源搜索引擎
## コントリビューター

View File

@@ -6,6 +6,8 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
@@ -28,7 +30,8 @@ import (
type Pan123 struct {
model.Storage
Addition
apiRateLimit sync.Map
apiRateLimit sync.Map
safeBoxUnlocked sync.Map
}
func (d *Pan123) Config() driver.Config {
@@ -52,9 +55,26 @@ func (d *Pan123) Drop(ctx context.Context) error {
}
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if f, ok := dir.(File); ok && f.IsLock {
if err := d.unlockSafeBox(f.FileId); err != nil {
return nil, err
}
}
files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
if err != nil {
return nil, err
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "safe box") || strings.Contains(err.Error(), "保险箱") {
if id, e := strconv.ParseInt(dir.GetID(), 10, 64); e == nil {
if e = d.unlockSafeBox(id); e == nil {
files, err = d.getFiles(ctx, dir.GetID(), dir.GetName())
} else {
return nil, e
}
}
}
if err != nil {
return nil, err
}
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return src, nil

View File

@@ -6,8 +6,9 @@ import (
)
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
SafePassword string `json:"safe_password"`
driver.RootID
//OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
//OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`

View File

@@ -20,6 +20,7 @@ type File struct {
Etag string `json:"Etag"`
S3KeyFlag string `json:"S3KeyFlag"`
DownloadUrl string `json:"DownloadUrl"`
IsLock bool `json:"IsLock"`
}
func (f File) CreateTime() time.Time {

View File

@@ -43,6 +43,7 @@ const (
S3Auth = MainApi + "/file/s3_upload_object/auth"
UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
S3Complete = MainApi + "/file/s3_complete_multipart_upload"
SafeBoxUnlock = MainApi + "/restful/goapi/v1/file/safe_box/auth/unlockbox"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
)
@@ -238,6 +239,22 @@ do:
return body, nil
}
func (d *Pan123) unlockSafeBox(fileId int64) error {
if _, ok := d.safeBoxUnlocked.Load(fileId); ok {
return nil
}
data := base.Json{"password": d.SafePassword}
url := fmt.Sprintf("%s?fileId=%d", SafeBoxUnlock, fileId)
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
if err != nil {
return err
}
d.safeBoxUnlocked.Store(fileId, true)
return nil
}
func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
page := 1
total := 0
@@ -267,6 +284,15 @@ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]
req.SetQueryParams(query)
}, &resp)
if err != nil {
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "safe box") || strings.Contains(err.Error(), "保险箱") {
if fid, e := strconv.ParseInt(parentId, 10, 64); e == nil {
if e = d.unlockSafeBox(fid); e == nil {
return d.getFiles(ctx, parentId, name)
}
return nil, e
}
}
return nil, err
}
log.Debug(string(_res))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -6,6 +6,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/115_share"
_ "github.com/alist-org/alist/v3/drivers/123"
_ "github.com/alist-org/alist/v3/drivers/123_link"
_ "github.com/alist-org/alist/v3/drivers/123_open"
_ "github.com/alist-org/alist/v3/drivers/123_share"
_ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189"
@@ -31,6 +32,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/github"
_ "github.com/alist-org/alist/v3/drivers/github_releases"
_ "github.com/alist-org/alist/v3/drivers/gofile"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
@@ -40,6 +42,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/lenovonas_share"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediafire"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/misskey"

261
drivers/gofile/driver.go Normal file
View File

@@ -0,0 +1,261 @@
package gofile
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
)
type Gofile struct {
model.Storage
Addition
accountId string
}
func (d *Gofile) Config() driver.Config {
return config
}
func (d *Gofile) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Gofile) Init(ctx context.Context) error {
if d.APIToken == "" {
return fmt.Errorf("API token is required")
}
// Get account ID
accountId, err := d.getAccountId(ctx)
if err != nil {
return fmt.Errorf("failed to get account ID: %w", err)
}
d.accountId = accountId
// Get account info to set root folder if not specified
if d.RootFolderID == "" {
accountInfo, err := d.getAccountInfo(ctx, accountId)
if err != nil {
return fmt.Errorf("failed to get account info: %w", err)
}
d.RootFolderID = accountInfo.Data.RootFolder
}
// Save driver storage
op.MustSaveDriverStorage(d)
return nil
}
func (d *Gofile) Drop(ctx context.Context) error {
return nil
}
func (d *Gofile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var folderId string
if dir.GetID() == "" {
folderId = d.GetRootId()
} else {
folderId = dir.GetID()
}
endpoint := fmt.Sprintf("/contents/%s", folderId)
var response ContentsResponse
err := d.getJSON(ctx, endpoint, &response)
if err != nil {
return nil, err
}
var objects []model.Obj
// Process children or contents
contents := response.Data.Children
if contents == nil {
contents = response.Data.Contents
}
for _, content := range contents {
objects = append(objects, d.convertContentToObj(content))
}
return objects, nil
}
func (d *Gofile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if file.IsDir() {
return nil, errs.NotFile
}
// Create a direct link for the file
directLink, err := d.createDirectLink(ctx, file.GetID())
if err != nil {
return nil, fmt.Errorf("failed to create direct link: %w", err)
}
return &model.Link{
URL: directLink,
}, nil
}
func (d *Gofile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
var parentId string
if parentDir.GetID() == "" {
parentId = d.GetRootId()
} else {
parentId = parentDir.GetID()
}
data := map[string]interface{}{
"parentFolderId": parentId,
"folderName": dirName,
}
var response CreateFolderResponse
err := d.postJSON(ctx, "/contents/createFolder", data, &response)
if err != nil {
return nil, err
}
return &model.Object{
ID: response.Data.ID,
Name: response.Data.Name,
IsFolder: true,
}, nil
}
func (d *Gofile) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var dstId string
if dstDir.GetID() == "" {
dstId = d.GetRootId()
} else {
dstId = dstDir.GetID()
}
data := map[string]interface{}{
"contentsId": srcObj.GetID(),
"folderId": dstId,
}
err := d.putJSON(ctx, "/contents/move", data, nil)
if err != nil {
return nil, err
}
// Return updated object
return &model.Object{
ID: srcObj.GetID(),
Name: srcObj.GetName(),
Size: srcObj.GetSize(),
Modified: srcObj.ModTime(),
IsFolder: srcObj.IsDir(),
}, nil
}
func (d *Gofile) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
data := map[string]interface{}{
"attribute": "name",
"attributeValue": newName,
}
var response UpdateResponse
err := d.putJSON(ctx, fmt.Sprintf("/contents/%s/update", srcObj.GetID()), data, &response)
if err != nil {
return nil, err
}
return &model.Object{
ID: srcObj.GetID(),
Name: newName,
Size: srcObj.GetSize(),
Modified: srcObj.ModTime(),
IsFolder: srcObj.IsDir(),
}, nil
}
func (d *Gofile) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var dstId string
if dstDir.GetID() == "" {
dstId = d.GetRootId()
} else {
dstId = dstDir.GetID()
}
data := map[string]interface{}{
"contentsId": srcObj.GetID(),
"folderId": dstId,
}
var response CopyResponse
err := d.postJSON(ctx, "/contents/copy", data, &response)
if err != nil {
return nil, err
}
// Get the new ID from the response
newId := srcObj.GetID()
if response.Data.CopiedContents != nil {
if id, ok := response.Data.CopiedContents[srcObj.GetID()]; ok {
newId = id
}
}
return &model.Object{
ID: newId,
Name: srcObj.GetName(),
Size: srcObj.GetSize(),
Modified: srcObj.ModTime(),
IsFolder: srcObj.IsDir(),
}, nil
}
func (d *Gofile) Remove(ctx context.Context, obj model.Obj) error {
data := map[string]interface{}{
"contentsId": obj.GetID(),
}
return d.deleteJSON(ctx, "/contents", data)
}
func (d *Gofile) Put(ctx context.Context, dstDir model.Obj, fileStreamer model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
var folderId string
if dstDir.GetID() == "" {
folderId = d.GetRootId()
} else {
folderId = dstDir.GetID()
}
response, err := d.uploadFile(ctx, folderId, fileStreamer, up)
if err != nil {
return nil, err
}
return &model.Object{
ID: response.Data.FileId,
Name: response.Data.FileName,
Size: fileStreamer.GetSize(),
IsFolder: false,
}, nil
}
func (d *Gofile) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
return nil, errs.NotImplement
}
func (d *Gofile) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
return nil, errs.NotImplement
}
func (d *Gofile) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
return nil, errs.NotImplement
}
func (d *Gofile) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
return nil, errs.NotImplement
}
var _ driver.Driver = (*Gofile)(nil)

26
drivers/gofile/meta.go Normal file
View File

@@ -0,0 +1,26 @@
package gofile
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
APIToken string `json:"api_token" required:"true" help:"Get your API token from your Gofile profile page"`
}
var config = driver.Config{
Name: "Gofile",
DefaultRoot: "",
LocalSort: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Gofile{}
})
}

124
drivers/gofile/types.go Normal file
View File

@@ -0,0 +1,124 @@
package gofile
import "time"
type APIResponse struct {
Status string `json:"status"`
Data interface{} `json:"data"`
}
type AccountResponse struct {
Status string `json:"status"`
Data struct {
ID string `json:"id"`
} `json:"data"`
}
type AccountInfoResponse struct {
Status string `json:"status"`
Data struct {
ID string `json:"id"`
Type string `json:"type"`
Email string `json:"email"`
RootFolder string `json:"rootFolder"`
} `json:"data"`
}
type Content struct {
ID string `json:"id"`
Type string `json:"type"` // "file" or "folder"
Name string `json:"name"`
Size int64 `json:"size,omitempty"`
CreateTime int64 `json:"createTime"`
ModTime int64 `json:"modTime,omitempty"`
DirectLink string `json:"directLink,omitempty"`
Children map[string]Content `json:"children,omitempty"`
ParentFolder string `json:"parentFolder,omitempty"`
MD5 string `json:"md5,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Link string `json:"link,omitempty"`
}
type ContentsResponse struct {
Status string `json:"status"`
Data struct {
IsOwner bool `json:"isOwner"`
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
ParentFolder string `json:"parentFolder"`
CreateTime int64 `json:"createTime"`
ChildrenList []string `json:"childrenList,omitempty"`
Children map[string]Content `json:"children,omitempty"`
Contents map[string]Content `json:"contents,omitempty"`
Public bool `json:"public,omitempty"`
Description string `json:"description,omitempty"`
Tags string `json:"tags,omitempty"`
Expiry int64 `json:"expiry,omitempty"`
} `json:"data"`
}
type UploadResponse struct {
Status string `json:"status"`
Data struct {
DownloadPage string `json:"downloadPage"`
Code string `json:"code"`
ParentFolder string `json:"parentFolder"`
FileId string `json:"fileId"`
FileName string `json:"fileName"`
GuestToken string `json:"guestToken,omitempty"`
} `json:"data"`
}
type DirectLinkResponse struct {
Status string `json:"status"`
Data struct {
DirectLink string `json:"directLink"`
ID string `json:"id"`
} `json:"data"`
}
type CreateFolderResponse struct {
Status string `json:"status"`
Data struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
ParentFolder string `json:"parentFolder"`
CreateTime int64 `json:"createTime"`
} `json:"data"`
}
type CopyResponse struct {
Status string `json:"status"`
Data struct {
CopiedContents map[string]string `json:"copiedContents"` // oldId -> newId mapping
} `json:"data"`
}
type UpdateResponse struct {
Status string `json:"status"`
Data struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"data"`
}
type ErrorResponse struct {
Status string `json:"status"`
Error struct {
Message string `json:"message"`
Code string `json:"code"`
} `json:"error"`
}
func (c *Content) ModifiedTime() time.Time {
if c.ModTime > 0 {
return time.Unix(c.ModTime, 0)
}
return time.Unix(c.CreateTime, 0)
}
func (c *Content) IsDir() bool {
return c.Type == "folder"
}

257
drivers/gofile/util.go Normal file
View File

@@ -0,0 +1,257 @@
package gofile
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
)
const (
baseAPI = "https://api.gofile.io"
uploadAPI = "https://upload.gofile.io"
)
func (d *Gofile) request(ctx context.Context, method, endpoint string, body io.Reader, headers map[string]string) (*http.Response, error) {
var url string
if strings.HasPrefix(endpoint, "http") {
url = endpoint
} else {
url = baseAPI + endpoint
}
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+d.APIToken)
req.Header.Set("User-Agent", "AList/3.0")
for k, v := range headers {
req.Header.Set(k, v)
}
return base.HttpClient.Do(req)
}
func (d *Gofile) getJSON(ctx context.Context, endpoint string, result interface{}) error {
resp, err := d.request(ctx, "GET", endpoint, nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return d.handleError(resp)
}
return json.NewDecoder(resp.Body).Decode(result)
}
func (d *Gofile) postJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
headers := map[string]string{
"Content-Type": "application/json",
}
resp, err := d.request(ctx, "POST", endpoint, bytes.NewBuffer(jsonData), headers)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return d.handleError(resp)
}
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
}
return nil
}
func (d *Gofile) putJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
headers := map[string]string{
"Content-Type": "application/json",
}
resp, err := d.request(ctx, "PUT", endpoint, bytes.NewBuffer(jsonData), headers)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return d.handleError(resp)
}
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
}
return nil
}
func (d *Gofile) deleteJSON(ctx context.Context, endpoint string, data interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
headers := map[string]string{
"Content-Type": "application/json",
}
resp, err := d.request(ctx, "DELETE", endpoint, bytes.NewBuffer(jsonData), headers)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return d.handleError(resp)
}
return nil
}
func (d *Gofile) handleError(resp *http.Response) error {
body, _ := io.ReadAll(resp.Body)
var errorResp ErrorResponse
if err := json.Unmarshal(body, &errorResp); err == nil {
return fmt.Errorf("gofile API error: %s (code: %s)", errorResp.Error.Message, errorResp.Error.Code)
}
return fmt.Errorf("gofile API error: HTTP %d - %s", resp.StatusCode, string(body))
}
func (d *Gofile) uploadFile(ctx context.Context, folderId string, file model.FileStreamer, up driver.UpdateProgress) (*UploadResponse, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if folderId != "" {
writer.WriteField("folderId", folderId)
}
part, err := writer.CreateFormFile("file", filepath.Base(file.GetName()))
if err != nil {
return nil, err
}
// Copy with progress tracking if available
if up != nil {
reader := &progressReader{
reader: file,
total: file.GetSize(),
up: up,
}
_, err = io.Copy(part, reader)
} else {
_, err = io.Copy(part, file)
}
if err != nil {
return nil, err
}
writer.Close()
headers := map[string]string{
"Content-Type": writer.FormDataContentType(),
}
resp, err := d.request(ctx, "POST", uploadAPI+"/uploadfile", &body, headers)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, d.handleError(resp)
}
var result UploadResponse
err = json.NewDecoder(resp.Body).Decode(&result)
return &result, err
}
func (d *Gofile) createDirectLink(ctx context.Context, contentId string) (string, error) {
data := map[string]interface{}{}
var result DirectLinkResponse
err := d.postJSON(ctx, fmt.Sprintf("/contents/%s/directlinks", contentId), data, &result)
if err != nil {
return "", err
}
return result.Data.DirectLink, nil
}
func (d *Gofile) convertContentToObj(content Content) model.Obj {
return &model.ObjThumb{
Object: model.Object{
ID: content.ID,
Name: content.Name,
Size: content.Size,
Modified: content.ModifiedTime(),
IsFolder: content.IsDir(),
},
}
}
func (d *Gofile) getAccountId(ctx context.Context) (string, error) {
var result AccountResponse
err := d.getJSON(ctx, "/accounts/getid", &result)
if err != nil {
return "", err
}
return result.Data.ID, nil
}
func (d *Gofile) getAccountInfo(ctx context.Context, accountId string) (*AccountInfoResponse, error) {
var result AccountInfoResponse
err := d.getJSON(ctx, fmt.Sprintf("/accounts/%s", accountId), &result)
if err != nil {
return nil, err
}
return &result, nil
}
// progressReader wraps an io.Reader to track upload progress
type progressReader struct {
reader io.Reader
total int64
read int64
up driver.UpdateProgress
}
func (pr *progressReader) Read(p []byte) (n int, err error) {
n, err = pr.reader.Read(p)
pr.read += int64(n)
if pr.up != nil && pr.total > 0 {
progress := float64(pr.read) * 100 / float64(pr.total)
pr.up(progress)
}
return n, err
}

View File

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

433
drivers/mediafire/driver.go Normal file
View File

@@ -0,0 +1,433 @@
package mediafire
/*
Package mediafire
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-11
D@' 3z K!7 - The King Of Cracking
*/
import (
"context"
"fmt"
"math/rand"
"net/http"
"os"
"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/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
)
type Mediafire struct {
model.Storage
Addition
cron *cron.Cron
actionToken string
appBase string
apiBase string
hostBase string
maxRetries int
secChUa string
secChUaPlatform string
userAgent string
}
func (d *Mediafire) Config() driver.Config {
return config
}
func (d *Mediafire) GetAddition() driver.Additional {
return &d.Addition
}
func (d *Mediafire) Init(ctx context.Context) error {
if d.SessionToken == "" {
return fmt.Errorf("Init :: [MediaFire] {critical} missing sessionToken")
}
if d.Cookie == "" {
return fmt.Errorf("Init :: [MediaFire] {critical} missing Cookie")
}
if _, err := d.getSessionToken(ctx); err != nil {
d.renewToken(ctx)
num := rand.Intn(4) + 6
d.cron = cron.NewCron(time.Minute * time.Duration(num))
d.cron.Do(func() {
d.renewToken(ctx)
})
}
return nil
}
func (d *Mediafire) Drop(ctx context.Context) error {
return nil
}
func (d *Mediafire) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(ctx, dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return d.fileToObj(src), nil
})
}
func (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadUrl, err := d.getDirectDownloadLink(ctx, file.GetID())
if err != nil {
return nil, err
}
res, err := base.NoRedirectClient.R().SetDoNotParseResponse(true).SetContext(ctx).Get(downloadUrl)
if err != nil {
return nil, err
}
defer func() {
_ = res.RawBody().Close()
}()
if res.StatusCode() == 302 {
downloadUrl = res.Header().Get("location")
}
return &model.Link{
URL: downloadUrl,
Header: http.Header{
"Origin": []string{d.appBase},
"Referer": []string{d.appBase + "/"},
"sec-ch-ua": []string{d.secChUa},
"sec-ch-ua-platform": []string{d.secChUaPlatform},
"User-Agent": []string{d.userAgent},
//"User-Agent": []string{base.UserAgent},
},
}, nil
}
func (d *Mediafire) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
data := map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"parent_key": parentDir.GetID(),
"foldername": dirName,
}
var resp MediafireFolderCreateResponse
_, err := d.postForm("/folder/create.php", data, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
created, _ := time.Parse("2006-01-02T15:04:05Z", resp.Response.CreatedUTC)
return &model.ObjThumb{
Object: model.Object{
ID: resp.Response.FolderKey,
Name: resp.Response.Name,
Size: 0,
Modified: created,
Ctime: created,
IsFolder: true,
},
Thumbnail: model.Thumbnail{},
}, nil
}
func (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var data map[string]string
var endpoint string
if srcObj.IsDir() {
endpoint = "/folder/move.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"folder_key_src": srcObj.GetID(),
"folder_key_dst": dstDir.GetID(),
}
} else {
endpoint = "/file/move.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"quick_key": srcObj.GetID(),
"folder_key": dstDir.GetID(),
}
}
var resp MediafireMoveResponse
_, err := d.postForm(endpoint, data, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
return srcObj, nil
}
func (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
var data map[string]string
var endpoint string
if srcObj.IsDir() {
endpoint = "/folder/update.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"folder_key": srcObj.GetID(),
"foldername": newName,
}
} else {
endpoint = "/file/update.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"quick_key": srcObj.GetID(),
"filename": newName,
}
}
var resp MediafireRenameResponse
_, err := d.postForm(endpoint, data, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
return &model.ObjThumb{
Object: model.Object{
ID: srcObj.GetID(),
Name: newName,
Size: srcObj.GetSize(),
Modified: srcObj.ModTime(),
Ctime: srcObj.CreateTime(),
IsFolder: srcObj.IsDir(),
},
Thumbnail: model.Thumbnail{},
}, nil
}
func (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var data map[string]string
var endpoint string
if srcObj.IsDir() {
endpoint = "/folder/copy.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"folder_key_src": srcObj.GetID(),
"folder_key_dst": dstDir.GetID(),
}
} else {
endpoint = "/file/copy.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"quick_key": srcObj.GetID(),
"folder_key": dstDir.GetID(),
}
}
var resp MediafireCopyResponse
_, err := d.postForm(endpoint, data, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
var newID string
if srcObj.IsDir() {
if len(resp.Response.NewFolderKeys) > 0 {
newID = resp.Response.NewFolderKeys[0]
}
} else {
if len(resp.Response.NewQuickKeys) > 0 {
newID = resp.Response.NewQuickKeys[0]
}
}
return &model.ObjThumb{
Object: model.Object{
ID: newID,
Name: srcObj.GetName(),
Size: srcObj.GetSize(),
Modified: srcObj.ModTime(),
Ctime: srcObj.CreateTime(),
IsFolder: srcObj.IsDir(),
},
Thumbnail: model.Thumbnail{},
}, nil
}
func (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error {
var data map[string]string
var endpoint string
if obj.IsDir() {
endpoint = "/folder/delete.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"folder_key": obj.GetID(),
}
} else {
endpoint = "/file/delete.php"
data = map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"quick_key": obj.GetID(),
}
}
var resp MediafireRemoveResponse
_, err := d.postForm(endpoint, data, &resp)
if err != nil {
return err
}
if resp.Response.Result != "Success" {
return fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
return nil
}
func (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
_, err := d.PutResult(ctx, dstDir, file, up)
return err
}
func (d *Mediafire) PutResult(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
}
defer tempFile.Close()
osFile, ok := tempFile.(*os.File)
if !ok {
return nil, fmt.Errorf("expected *os.File, got %T", tempFile)
}
fileHash, err := d.calculateSHA256(osFile)
if err != nil {
return nil, err
}
checkResp, err := d.uploadCheck(ctx, file.GetName(), file.GetSize(), fileHash, dstDir.GetID())
if err != nil {
return nil, err
}
if checkResp.Response.ResumableUpload.AllUnitsReady == "yes" {
up(100.0)
}
if checkResp.Response.HashExists == "yes" && checkResp.Response.InAccount == "yes" {
up(100.0)
existingFile, err := d.getExistingFileInfo(ctx, fileHash, file.GetName(), dstDir.GetID())
if err == nil {
return existingFile, nil
}
}
var pollKey string
if checkResp.Response.ResumableUpload.AllUnitsReady != "yes" {
var err error
pollKey, err = d.uploadUnits(ctx, osFile, checkResp, file.GetName(), fileHash, dstDir.GetID(), up)
if err != nil {
return nil, err
}
} else {
pollKey = checkResp.Response.ResumableUpload.UploadKey
}
//fmt.Printf("pollKey: %+v\n", pollKey)
pollResp, err := d.pollUpload(ctx, pollKey)
if err != nil {
return nil, err
}
quickKey := pollResp.Response.Doupload.QuickKey
return &model.ObjThumb{
Object: model.Object{
ID: quickKey,
Name: file.GetName(),
Size: file.GetSize(),
},
Thumbnail: model.Thumbnail{},
}, nil
}
func (d *Mediafire) 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 *Mediafire) 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 *Mediafire) 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 *Mediafire) 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 *Mediafire) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Mediafire)(nil)

54
drivers/mediafire/meta.go Normal file
View File

@@ -0,0 +1,54 @@
package mediafire
/*
Package mediafire
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-11
D@' 3z K!7 - The King Of Cracking
*/
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
//driver.RootID
SessionToken string `json:"session_token" required:"true" type:"string" help:"Required for MediaFire API"`
Cookie string `json:"cookie" required:"true" type:"string" help:"Required for navigation"`
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
}
var config = driver.Config{
Name: "MediaFire",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &Mediafire{
appBase: "https://app.mediafire.com",
apiBase: "https://www.mediafire.com/api/1.5",
hostBase: "https://www.mediafire.com",
maxRetries: 3,
secChUa: "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"139\", \"Google Chrome\";v=\"139\"",
secChUaPlatform: "Windows",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
}
})
}

232
drivers/mediafire/types.go Normal file
View File

@@ -0,0 +1,232 @@
package mediafire
/*
Package mediafire
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-11
D@' 3z K!7 - The King Of Cracking
*/
type MediafireRenewTokenResponse struct {
Response struct {
Action string `json:"action"`
SessionToken string `json:"session_token"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafireResponse struct {
Response struct {
Action string `json:"action"`
FolderContent struct {
ChunkSize string `json:"chunk_size"`
ContentType string `json:"content_type"`
ChunkNumber string `json:"chunk_number"`
FolderKey string `json:"folderkey"`
Folders []MediafireFolder `json:"folders,omitempty"`
Files []MediafireFile `json:"files,omitempty"`
MoreChunks string `json:"more_chunks"`
} `json:"folder_content"`
Result string `json:"result"`
} `json:"response"`
}
type MediafireFolder struct {
FolderKey string `json:"folderkey"`
Name string `json:"name"`
Created string `json:"created"`
CreatedUTC string `json:"created_utc"`
}
type MediafireFile struct {
QuickKey string `json:"quickkey"`
Filename string `json:"filename"`
Size string `json:"size"`
Created string `json:"created"`
CreatedUTC string `json:"created_utc"`
MimeType string `json:"mimetype"`
}
type File struct {
ID string
Name string
Size int64
CreatedUTC string
IsFolder bool
}
type FolderContentResponse struct {
Folders []MediafireFolder
Files []MediafireFile
MoreChunks bool
}
type MediafireLinksResponse struct {
Response struct {
Action string `json:"action"`
Links []struct {
QuickKey string `json:"quickkey"`
View string `json:"view"`
NormalDownload string `json:"normal_download"`
OneTime struct {
Download string `json:"download"`
View string `json:"view"`
} `json:"one_time"`
} `json:"links"`
OneTimeKeyRequestCount string `json:"one_time_key_request_count"`
OneTimeKeyRequestMaxCount string `json:"one_time_key_request_max_count"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafireDirectDownloadResponse struct {
Response struct {
Action string `json:"action"`
Links []struct {
QuickKey string `json:"quickkey"`
DirectDownload string `json:"direct_download"`
} `json:"links"`
DirectDownloadFreeBandwidth string `json:"direct_download_free_bandwidth"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafireFolderCreateResponse struct {
Response struct {
Action string `json:"action"`
FolderKey string `json:"folder_key"`
UploadKey string `json:"upload_key"`
ParentFolderKey string `json:"parent_folderkey"`
Name string `json:"name"`
Description string `json:"description"`
Created string `json:"created"`
CreatedUTC string `json:"created_utc"`
Privacy string `json:"privacy"`
FileCount string `json:"file_count"`
FolderCount string `json:"folder_count"`
Revision string `json:"revision"`
DropboxEnabled string `json:"dropbox_enabled"`
Flag string `json:"flag"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
NewDeviceRevision int `json:"new_device_revision"`
} `json:"response"`
}
type MediafireMoveResponse struct {
Response struct {
Action string `json:"action"`
Asynchronous string `json:"asynchronous,omitempty"`
NewNames []string `json:"new_names"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
NewDeviceRevision int `json:"new_device_revision"`
} `json:"response"`
}
type MediafireRenameResponse struct {
Response struct {
Action string `json:"action"`
Asynchronous string `json:"asynchronous,omitempty"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
NewDeviceRevision int `json:"new_device_revision"`
} `json:"response"`
}
type MediafireCopyResponse struct {
Response struct {
Action string `json:"action"`
Asynchronous string `json:"asynchronous,omitempty"`
NewQuickKeys []string `json:"new_quickkeys,omitempty"`
NewFolderKeys []string `json:"new_folderkeys,omitempty"`
SkippedCount string `json:"skipped_count,omitempty"`
OtherCount string `json:"other_count,omitempty"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
NewDeviceRevision int `json:"new_device_revision"`
} `json:"response"`
}
type MediafireRemoveResponse struct {
Response struct {
Action string `json:"action"`
Asynchronous string `json:"asynchronous,omitempty"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
NewDeviceRevision int `json:"new_device_revision"`
} `json:"response"`
}
type MediafireCheckResponse struct {
Response struct {
Action string `json:"action"`
HashExists string `json:"hash_exists"`
InAccount string `json:"in_account"`
InFolder string `json:"in_folder"`
FileExists string `json:"file_exists"`
ResumableUpload struct {
AllUnitsReady string `json:"all_units_ready"`
NumberOfUnits string `json:"number_of_units"`
UnitSize string `json:"unit_size"`
Bitmap struct {
Count string `json:"count"`
Words []string `json:"words"`
} `json:"bitmap"`
UploadKey string `json:"upload_key"`
} `json:"resumable_upload"`
AvailableSpace string `json:"available_space"`
UsedStorageSize string `json:"used_storage_size"`
StorageLimit string `json:"storage_limit"`
StorageLimitExceeded string `json:"storage_limit_exceeded"`
UploadURL struct {
Simple string `json:"simple"`
SimpleFallback string `json:"simple_fallback"`
Resumable string `json:"resumable"`
ResumableFallback string `json:"resumable_fallback"`
} `json:"upload_url"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafireActionTokenResponse struct {
Response struct {
Action string `json:"action"`
ActionToken string `json:"action_token"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafirePollResponse struct {
Response struct {
Action string `json:"action"`
Doupload struct {
Result string `json:"result"`
Status string `json:"status"`
Description string `json:"description"`
QuickKey string `json:"quickkey"`
Hash string `json:"hash"`
Filename string `json:"filename"`
Size string `json:"size"`
Created string `json:"created"`
CreatedUTC string `json:"created_utc"`
Revision string `json:"revision"`
} `json:"doupload"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}
type MediafireFileSearchResponse struct {
Response struct {
Action string `json:"action"`
FileInfo []File `json:"file_info"`
Result string `json:"result"`
CurrentAPIVersion string `json:"current_api_version"`
} `json:"response"`
}

626
drivers/mediafire/util.go Normal file
View File

@@ -0,0 +1,626 @@
package mediafire
/*
Package mediafire
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-11
D@' 3z K!7 - The King Of Cracking
*/
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"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/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {
tokenURL := d.hostBase + "/application/get_session_token.php"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Content-Length", "0")
req.Header.Set("Cookie", d.Cookie)
req.Header.Set("DNT", "1")
req.Header.Set("Origin", d.hostBase)
req.Header.Set("Priority", "u=1, i")
req.Header.Set("Referer", (d.hostBase + "/"))
req.Header.Set("Sec-Ch-Ua", d.secChUa)
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", d.secChUaPlatform)
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-site")
req.Header.Set("User-Agent", d.userAgent)
//req.Header.Set("Connection", "keep-alive")
resp, err := base.HttpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
//fmt.Printf("getSessionToken :: Raw response: %s\n", string(body))
//fmt.Printf("getSessionToken :: Parsed response: %+v\n", resp)
var tokenResp struct {
Response struct {
SessionToken string `json:"session_token"`
} `json:"response"`
}
if resp.StatusCode == 200 {
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", err
}
if tokenResp.Response.SessionToken == "" {
return "", fmt.Errorf("empty session token received")
}
cookieMap := make(map[string]string)
for _, cookie := range resp.Cookies() {
cookieMap[cookie.Name] = cookie.Value
}
if len(cookieMap) > 0 {
var cookies []string
for name, value := range cookieMap {
cookies = append(cookies, fmt.Sprintf("%s=%s", name, value))
}
d.Cookie = strings.Join(cookies, "; ")
op.MustSaveDriverStorage(d)
//fmt.Printf("getSessionToken :: Captured cookies: %s\n", d.Cookie)
}
} else {
return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode)
}
d.SessionToken = tokenResp.Response.SessionToken
//fmt.Printf("Init :: Obtain Session Token %v", d.SessionToken)
op.MustSaveDriverStorage(d)
return d.SessionToken, nil
}
func (d *Mediafire) renewToken(_ context.Context) error {
query := map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
}
var resp MediafireRenewTokenResponse
_, err := d.postForm("/user/renew_session_token.php", query, &resp)
if err != nil {
return fmt.Errorf("failed to renew token: %w", err)
}
//fmt.Printf("getInfo :: Raw response: %s\n", string(body))
//fmt.Printf("getInfo :: Parsed response: %+v\n", resp)
if resp.Response.Result != "Success" {
return fmt.Errorf("MediaFire token renewal failed: %s", resp.Response.Result)
}
d.SessionToken = resp.Response.SessionToken
//fmt.Printf("Init :: Renew Session Token: %s", resp.Response.Result)
op.MustSaveDriverStorage(d)
return nil
}
func (d *Mediafire) getFiles(ctx context.Context, folderKey string) ([]File, error) {
files := make([]File, 0)
hasMore := true
chunkNumber := 1
for hasMore {
resp, err := d.getFolderContent(ctx, folderKey, chunkNumber)
if err != nil {
return nil, err
}
for _, folder := range resp.Folders {
files = append(files, File{
ID: folder.FolderKey,
Name: folder.Name,
Size: 0,
CreatedUTC: folder.CreatedUTC,
IsFolder: true,
})
}
for _, file := range resp.Files {
size, _ := strconv.ParseInt(file.Size, 10, 64)
files = append(files, File{
ID: file.QuickKey,
Name: file.Filename,
Size: size,
CreatedUTC: file.CreatedUTC,
IsFolder: false,
})
}
hasMore = resp.MoreChunks
chunkNumber++
}
return files, nil
}
func (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chunkNumber int) (*FolderContentResponse, error) {
foldersResp, err := d.getFolderContentByType(ctx, folderKey, "folders", chunkNumber)
if err != nil {
return nil, err
}
filesResp, err := d.getFolderContentByType(ctx, folderKey, "files", chunkNumber)
if err != nil {
return nil, err
}
return &FolderContentResponse{
Folders: foldersResp.Response.FolderContent.Folders,
Files: filesResp.Response.FolderContent.Files,
MoreChunks: foldersResp.Response.FolderContent.MoreChunks == "yes" || filesResp.Response.FolderContent.MoreChunks == "yes",
}, nil
}
func (d *Mediafire) getFolderContentByType(_ context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) {
data := map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"folder_key": folderKey,
"content_type": contentType,
"chunk": strconv.Itoa(chunkNumber),
"chunk_size": strconv.FormatInt(d.ChunkSize, 10),
"details": "yes",
"order_direction": d.OrderDirection,
"order_by": d.OrderBy,
"filter": "",
}
var resp MediafireResponse
_, err := d.postForm("/folder/get_content.php", data, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
return &resp, nil
}
func (d *Mediafire) fileToObj(f File) *model.ObjThumb {
created, _ := time.Parse("2006-01-02T15:04:05Z", f.CreatedUTC)
var thumbnailURL string
if !f.IsFolder && f.ID != "" {
thumbnailURL = d.hostBase + "/convkey/acaa/" + f.ID + "3g.jpg"
}
return &model.ObjThumb{
Object: model.Object{
ID: f.ID,
//Path: "",
Name: f.Name,
Size: f.Size,
Modified: created,
Ctime: created,
IsFolder: f.IsFolder,
},
Thumbnail: model.Thumbnail{
Thumbnail: thumbnailURL,
},
}
}
func (d *Mediafire) getForm(endpoint string, query map[string]string, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetQueryParams(query)
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
//"User-Agent": base.UserAgent,
"User-Agent": d.userAgent,
"Origin": d.appBase,
"Referer": d.appBase + "/",
})
// If response OK
if resp != nil {
req.SetResult(resp)
}
// Targets MediaFire API
res, err := req.Get(d.apiBase + endpoint)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *Mediafire) postForm(endpoint string, data map[string]string, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetFormData(data)
req.SetHeaders(map[string]string{
"Cookie": d.Cookie,
"Content-Type": "application/x-www-form-urlencoded",
//"User-Agent": base.UserAgent,
"User-Agent": d.userAgent,
"Origin": d.appBase,
"Referer": d.appBase + "/",
})
// If response OK
if resp != nil {
req.SetResult(resp)
}
// Targets MediaFire API
res, err := req.Post(d.apiBase + endpoint)
if err != nil {
return nil, err
}
return res.Body(), nil
}
func (d *Mediafire) getDirectDownloadLink(_ context.Context, fileID string) (string, error) {
data := map[string]string{
"session_token": d.SessionToken,
"quick_key": fileID,
"link_type": "direct_download",
"response_format": "json",
}
var resp MediafireDirectDownloadResponse
_, err := d.getForm("/file/get_links.php", data, &resp)
if err != nil {
return "", err
}
if resp.Response.Result != "Success" {
return "", fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
}
if len(resp.Response.Links) == 0 {
return "", fmt.Errorf("no download links found")
}
return resp.Response.Links[0].DirectDownload, nil
}
func (d *Mediafire) calculateSHA256(file *os.File) (string, error) {
hasher := sha256.New()
if _, err := file.Seek(0, 0); err != nil {
return "", err
}
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {
actionToken, err := d.getActionToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get action token: %w", err)
}
query := map[string]string{
"session_token": actionToken, /* d.SessionToken */
"filename": filename,
"size": strconv.FormatInt(filesize, 10),
"hash": filehash,
"folder_key": folderKey,
"resumable": "yes",
"response_format": "json",
}
var resp MediafireCheckResponse
_, err = d.postForm("/upload/check.php", query, &resp)
if err != nil {
return nil, err
}
//fmt.Printf("uploadCheck :: Raw response: %s\n", string(body))
//fmt.Printf("uploadCheck :: Parsed response: %+v\n", resp)
//fmt.Printf("uploadCheck :: ResumableUpload section: %+v\n", resp.Response.ResumableUpload)
//fmt.Printf("uploadCheck :: Upload key specifically: '%s'\n", resp.Response.ResumableUpload.UploadKey)
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire upload check failed: %s", resp.Response.Result)
}
return &resp, nil
}
func (d *Mediafire) resumableUpload(ctx context.Context, folderKey, uploadKey string, unitData []byte, unitID int, fileHash, filename string, totalFileSize int64) (string, error) {
actionToken, err := d.getActionToken(ctx)
if err != nil {
return "", err
}
url := d.apiBase + "/upload/resumable.php"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(unitData))
if err != nil {
return "", err
}
q := req.URL.Query()
q.Add("folder_key", folderKey)
q.Add("response_format", "json")
q.Add("session_token", actionToken)
q.Add("key", uploadKey)
req.URL.RawQuery = q.Encode()
req.Header.Set("x-filehash", fileHash)
req.Header.Set("x-filesize", strconv.FormatInt(totalFileSize, 10))
req.Header.Set("x-unit-id", strconv.Itoa(unitID))
req.Header.Set("x-unit-size", strconv.FormatInt(int64(len(unitData)), 10))
req.Header.Set("x-unit-hash", d.sha256Hex(bytes.NewReader(unitData)))
req.Header.Set("x-filename", filename)
req.Header.Set("Content-Type", "application/octet-stream")
req.ContentLength = int64(len(unitData))
/* fmt.Printf("Debug resumable upload request:\n")
fmt.Printf(" URL: %s\n", req.URL.String())
fmt.Printf(" Headers: %+v\n", req.Header)
fmt.Printf(" Unit ID: %d\n", unitID)
fmt.Printf(" Unit Size: %d\n", len(unitData))
fmt.Printf(" Upload Key: %s\n", uploadKey)
fmt.Printf(" Action Token: %s\n", actionToken) */
res, err := base.HttpClient.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %v", err)
}
//fmt.Printf("MediaFire resumable upload response (status %d): %s\n", res.StatusCode, string(body))
var uploadResp struct {
Response struct {
Doupload struct {
Key string `json:"key"`
} `json:"doupload"`
Result string `json:"result"`
} `json:"response"`
}
if err := json.Unmarshal(body, &uploadResp); err != nil {
return "", fmt.Errorf("failed to parse response: %v", err)
}
if res.StatusCode != 200 {
return "", fmt.Errorf("resumable upload failed with status %d", res.StatusCode)
}
return uploadResp.Response.Doupload.Key, nil
}
func (d *Mediafire) uploadUnits(ctx context.Context, file *os.File, checkResp *MediafireCheckResponse, filename, fileHash, folderKey string, up driver.UpdateProgress) (string, error) {
unitSize, _ := strconv.ParseInt(checkResp.Response.ResumableUpload.UnitSize, 10, 64)
numUnits, _ := strconv.Atoi(checkResp.Response.ResumableUpload.NumberOfUnits)
uploadKey := checkResp.Response.ResumableUpload.UploadKey
stringWords := checkResp.Response.ResumableUpload.Bitmap.Words
intWords := make([]int, len(stringWords))
for i, word := range stringWords {
intWords[i], _ = strconv.Atoi(word)
}
var finalUploadKey string
for unitID := 0; unitID < numUnits; unitID++ {
if utils.IsCanceled(ctx) {
return "", ctx.Err()
}
if d.isUnitUploaded(intWords, unitID) {
up(float64(unitID+1) * 100 / float64(numUnits))
continue
}
uploadKey, err := d.uploadSingleUnit(ctx, file, unitID, unitSize, fileHash, filename, uploadKey, folderKey)
if err != nil {
return "", err
}
finalUploadKey = uploadKey
up(float64(unitID+1) * 100 / float64(numUnits))
}
return finalUploadKey, nil
}
func (d *Mediafire) uploadSingleUnit(ctx context.Context, file *os.File, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string) (string, error) {
start := int64(unitID) * unitSize
size := unitSize
stat, err := file.Stat()
if err != nil {
return "", err
}
fileSize := stat.Size()
if start+size > fileSize {
size = fileSize - start
}
unitData := make([]byte, size)
if _, err := file.ReadAt(unitData, start); err != nil {
return "", err
}
return d.resumableUpload(ctx, folderKey, uploadKey, unitData, unitID, fileHash, filename, fileSize)
}
func (d *Mediafire) getActionToken(_ context.Context) (string, error) {
if d.actionToken != "" {
return d.actionToken, nil
}
data := map[string]string{
"type": "upload",
"lifespan": "1440",
"response_format": "json",
"session_token": d.SessionToken,
}
var resp MediafireActionTokenResponse
_, err := d.postForm("/user/get_action_token.php", data, &resp)
if err != nil {
return "", err
}
if resp.Response.Result != "Success" {
return "", fmt.Errorf("MediaFire action token failed: %s", resp.Response.Result)
}
return resp.Response.ActionToken, nil
}
func (d *Mediafire) pollUpload(ctx context.Context, key string) (*MediafirePollResponse, error) {
actionToken, err := d.getActionToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get action token: %w", err)
}
//fmt.Printf("Debug Key: %+v\n", key)
query := map[string]string{
"key": key,
"response_format": "json",
"session_token": actionToken, /* d.SessionToken */
}
var resp MediafirePollResponse
_, err = d.postForm("/upload/poll_upload.php", query, &resp)
if err != nil {
return nil, err
}
//fmt.Printf("pollUpload :: Raw response: %s\n", string(body))
//fmt.Printf("pollUpload :: Parsed response: %+v\n", resp)
//fmt.Printf("pollUpload :: Debug Result: %+v\n", resp.Response.Result)
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire poll upload failed: %s", resp.Response.Result)
}
return &resp, nil
}
func (d *Mediafire) sha256Hex(r io.Reader) string {
h := sha256.New()
io.Copy(h, r)
return hex.EncodeToString(h.Sum(nil))
}
func (d *Mediafire) isUnitUploaded(words []int, unitID int) bool {
wordIndex := unitID / 16
bitIndex := unitID % 16
if wordIndex >= len(words) {
return false
}
return (words[wordIndex]>>bitIndex)&1 == 1
}
func (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, folderKey string) (*model.ObjThumb, error) {
if fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {
return fileInfo, nil
}
files, err := d.getFiles(ctx, folderKey)
if err != nil {
return nil, err
}
for _, file := range files {
if file.Name == filename && !file.IsFolder {
return d.fileToObj(file), nil
}
}
return nil, fmt.Errorf("existing file not found")
}
func (d *Mediafire) getFileByHash(_ context.Context, hash string) (*model.ObjThumb, error) {
query := map[string]string{
"session_token": d.SessionToken,
"response_format": "json",
"hash": hash,
}
var resp MediafireFileSearchResponse
_, err := d.postForm("/file/get_info.php", query, &resp)
if err != nil {
return nil, err
}
if resp.Response.Result != "Success" {
return nil, fmt.Errorf("MediaFire file search failed: %s", resp.Response.Result)
}
if len(resp.Response.FileInfo) == 0 {
return nil, fmt.Errorf("file not found by hash")
}
file := resp.Response.FileInfo[0]
return d.fileToObj(file), nil
}

13
go.mod
View File

@@ -3,10 +3,12 @@ module github.com/alist-org/alist/v3
go 1.23.4
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
github.com/KirCute/sftpd-alist v0.0.12
github.com/ProtonMail/go-crypto v1.0.0
github.com/SheltonZhu/115driver v1.0.34
github.com/SheltonZhu/115driver v1.1.2
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
github.com/alist-org/gofakes3 v0.0.7
@@ -79,11 +81,7 @@ require (
gorm.io/gorm v1.25.11
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
)
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
require (
github.com/STARRY-S/zip v0.2.1 // indirect
@@ -109,7 +107,6 @@ require (
github.com/ipfs/boxo v0.12.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78
@@ -268,4 +265,4 @@ require (
lukechampine.com/blake3 v1.1.7 // indirect
)
// replace github.com/xhofe/115-sdk-go => ../../xhofe/115-sdk-go
replace github.com/SheltonZhu/115driver => github.com/okatu-loli/115driver v1.1.2

17
go.sum
View File

@@ -21,10 +21,16 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -40,8 +46,6 @@ github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
github.com/SheltonZhu/115driver v1.0.34 h1:zhMLp4vgq7GksqvSxQQDOVfK6EOHldQl4b2n8tnZ+EE=
github.com/SheltonZhu/115driver v1.0.34/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21 h1:h6q5E9aMBhhdqouW81LozVPI1I+Pu6IxL2EKpfm5OjY=
@@ -172,7 +176,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -398,6 +401,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1 h1:DLQQEgHUAGZB6RVlceB1f6A94O206exxW2RIMH+gMUc=
github.com/larksuite/oapi-sdk-go/v3 v3.3.1/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -483,6 +488,8 @@ github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY=
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/okatu-loli/115driver v1.1.2 h1:XZT3r/51SZRQGzre2IeA+0/k4T1FneqArdhE4Wd600Q=
github.com/okatu-loli/115driver v1.1.2/go.mod h1:rKvNd4Y4OkXv1TMbr/SKjGdcvMQxh6AW5Tw9w0CJb7E=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
@@ -492,6 +499,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -739,8 +748,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

View File

@@ -91,6 +91,7 @@ func InitialSettings() []model.SettingItem {
} else {
token = random.Token()
}
defaultRoleID := strconv.Itoa(model.GUEST)
initialSettingItems = []model.SettingItem{
// site settings
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
@@ -103,6 +104,8 @@ func InitialSettings() []model.SettingItem {
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
{Key: conf.AllowRegister, Value: "false", Type: conf.TypeBool, Group: model.SITE},
{Key: conf.DefaultRole, Value: defaultRoleID, Type: conf.TypeSelect, Group: model.SITE},
// newui settings
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
// style settings
@@ -162,6 +165,9 @@ func InitialSettings() []model.SettingItem {
{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.WebauthnLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PUBLIC},
{Key: conf.MaxDevices, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL},
{Key: conf.DeviceEvictPolicy, Value: "deny", Type: conf.TypeSelect, Options: "deny,evict_oldest", Group: model.GLOBAL},
{Key: conf.DeviceSessionTTL, Value: "86400", Type: conf.TypeNumber, Group: model.GLOBAL},
// single settings
{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},

View File

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

View File

@@ -10,13 +10,15 @@ const (
const (
// site
VERSION = "version"
SiteTitle = "site_title"
Announcement = "announcement"
AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
UseNewui = "use_newui"
VERSION = "version"
SiteTitle = "site_title"
Announcement = "announcement"
AllowIndexed = "allow_indexed"
AllowMounted = "allow_mounted"
RobotsTxt = "robots_txt"
AllowRegister = "allow_register"
DefaultRole = "default_role"
UseNewui = "use_newui"
Logo = "logo"
Favicon = "favicon"
@@ -46,6 +48,9 @@ const (
ForwardDirectLinkParams = "forward_direct_link_params"
IgnoreDirectLinkParams = "ignore_direct_link_params"
WebauthnLoginEnabled = "webauthn_login_enabled"
MaxDevices = "max_devices"
DeviceEvictPolicy = "device_evict_policy"
DeviceSessionTTL = "device_session_ttl"
// index
SearchIndex = "search_index"

View File

@@ -12,7 +12,7 @@ var db *gorm.DB
func Init(d *gorm.DB) {
db = d
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.Role), new(model.Label), new(model.LabelFileBinDing), new(model.ObjFile))
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.Role), new(model.Label), new(model.LabelFileBinding), new(model.ObjFile), new(model.Session))
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}

View File

@@ -1,15 +1,18 @@
package db
import (
"fmt"
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"time"
)
// GetLabelIds Get all label_ids from database order by file_name
func GetLabelIds(userId uint, fileName string) ([]uint, error) {
labelFileBinDingDB := db.Model(&model.LabelFileBinDing{})
//fmt.Printf(">>> [GetLabelIds] userId: %d, fileName: %s\n", userId, fileName)
labelFileBinDingDB := db.Model(&model.LabelFileBinding{})
var labelIds []uint
if err := labelFileBinDingDB.Where("file_name = ?", fileName).Where("user_id = ?", userId).Pluck("label_id", &labelIds).Error; err != nil {
return nil, errors.WithStack(err)
@@ -18,7 +21,7 @@ func GetLabelIds(userId uint, fileName string) ([]uint, error) {
}
func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
var labelFileBinDing model.LabelFileBinDing
var labelFileBinDing model.LabelFileBinding
labelFileBinDing.UserId = userId
labelFileBinDing.LabelId = labelId
labelFileBinDing.FileName = fileName
@@ -32,7 +35,7 @@ func CreateLabelFileBinDing(fileName string, labelId, userId uint) error {
// GetLabelFileBinDingByLabelIdExists Get Label by label_id, used to del label usually
func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
var labelFileBinDing model.LabelFileBinDing
var labelFileBinDing model.LabelFileBinding
result := db.Where("label_id = ?", labelId).Where("user_id = ?", userId).First(&labelFileBinDing)
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound)
return exists
@@ -40,17 +43,150 @@ func GetLabelFileBinDingByLabelIdExists(labelId, userId uint) bool {
// DelLabelFileBinDingByFileName used to del usually
func DelLabelFileBinDingByFileName(userId uint, fileName string) error {
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
return errors.WithStack(db.Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
}
// DelLabelFileBinDingById used to del usually
func DelLabelFileBinDingById(labelId, userId uint, fileName string) error {
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinDing{}).Error)
return errors.WithStack(db.Where("label_id = ?", labelId).Where("file_name = ?", fileName).Where("user_id = ?", userId).Delete(model.LabelFileBinding{}).Error)
}
func GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinDing, err error) {
func GetLabelFileBinDingByLabelId(labelIds []uint, userId uint) (result []model.LabelFileBinding, err error) {
if err := db.Where("label_id in (?)", labelIds).Where("user_id = ?", userId).Find(&result).Error; err != nil {
return nil, errors.WithStack(err)
}
return result, nil
}
func GetLabelBindingsByFileNamesPublic(fileNames []string) (map[string][]uint, error) {
var binds []model.LabelFileBinding
if err := db.Where("file_name IN ?", fileNames).Find(&binds).Error; err != nil {
return nil, errors.WithStack(err)
}
out := make(map[string][]uint, len(fileNames))
seen := make(map[string]struct{}, len(binds))
for _, b := range binds {
key := fmt.Sprintf("%s-%d", b.FileName, b.LabelId)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out[b.FileName] = append(out[b.FileName], b.LabelId)
}
return out, nil
}
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
bindMap, err := GetLabelBindingsByFileNamesPublic(fileNames)
if err != nil {
return nil, err
}
idSet := make(map[uint]struct{})
for _, ids := range bindMap {
for _, id := range ids {
idSet[id] = struct{}{}
}
}
if len(idSet) == 0 {
return make(map[string][]model.Label, 0), nil
}
allIDs := make([]uint, 0, len(idSet))
for id := range idSet {
allIDs = append(allIDs, id)
}
labels, err := GetLabelByIds(allIDs) // 你已有的函数
if err != nil {
return nil, err
}
labelByID := make(map[uint]model.Label, len(labels))
for _, l := range labels {
labelByID[l.ID] = l
}
out := make(map[string][]model.Label, len(bindMap))
for fname, ids := range bindMap {
for _, id := range ids {
if lab, ok := labelByID[id]; ok {
out[fname] = append(out[fname], lab)
}
}
}
return out, nil
}
func ListLabelFileBinDing(userId uint, labelIDs []uint, fileName string, page, pageSize int) ([]model.LabelFileBinding, int64, error) {
q := db.Model(&model.LabelFileBinding{}).Where("user_id = ?", userId)
if len(labelIDs) > 0 {
q = q.Where("label_id IN ?", labelIDs)
}
if fileName != "" {
q = q.Where("file_name LIKE ?", "%"+fileName+"%")
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, errors.WithStack(err)
}
var rows []model.LabelFileBinding
if err := q.
Order("id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&rows).Error; err != nil {
return nil, 0, errors.WithStack(err)
}
return rows, total, nil
}
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
if len(bindings) == 0 {
return nil
}
tx := db.Begin()
if override {
type key struct {
uid uint
name string
}
toDel := make(map[key]struct{}, len(bindings))
for i := range bindings {
k := key{uid: bindings[i].UserId, name: bindings[i].FileName}
toDel[k] = struct{}{}
}
for k := range toDel {
if err := tx.Where("user_id = ? AND file_name = ?", k.uid, k.name).
Delete(&model.LabelFileBinding{}).Error; err != nil {
tx.Rollback()
return errors.WithStack(err)
}
}
}
for i := range bindings {
b := bindings[i]
if !keepIDs {
b.ID = 0
}
if b.CreateTime.IsZero() {
b.CreateTime = time.Now()
}
if override {
if err := tx.Create(&b).Error; err != nil {
tx.Rollback()
return errors.WithStack(err)
}
} else {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&b).Error; err != nil {
tx.Rollback()
return errors.WithStack(err)
}
}
}
return errors.WithStack(tx.Commit().Error)
}

View File

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

69
internal/db/session.go Normal file
View File

@@ -0,0 +1,69 @@
package db
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
"gorm.io/gorm/clause"
)
func GetSession(userID uint, deviceKey string) (*model.Session, error) {
s := model.Session{UserID: userID, DeviceKey: deviceKey}
if err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where(&s).First(&s).Error; err != nil {
return nil, errors.Wrap(err, "failed find session")
}
return &s, nil
}
func CreateSession(s *model.Session) error {
return errors.WithStack(db.Create(s).Error)
}
func UpsertSession(s *model.Session) error {
return errors.WithStack(db.Clauses(clause.OnConflict{UpdateAll: true}).Create(s).Error)
}
func DeleteSession(userID uint, deviceKey string) error {
return errors.WithStack(db.Where("user_id = ? AND device_key = ?", userID, deviceKey).Delete(&model.Session{}).Error)
}
func CountActiveSessionsByUser(userID uint) (int64, error) {
var count int64
err := db.Model(&model.Session{}).
Where("user_id = ? AND status = ?", userID, model.SessionActive).
Count(&count).Error
return count, errors.WithStack(err)
}
func DeleteSessionsBefore(ts int64) error {
return errors.WithStack(db.Where("last_active < ?", ts).Delete(&model.Session{}).Error)
}
// GetOldestActiveSession returns the oldest active session for the specified user.
func GetOldestActiveSession(userID uint) (*model.Session, error) {
var s model.Session
if err := db.Where("user_id = ? AND status = ?", userID, model.SessionActive).
Order("last_active ASC").First(&s).Error; err != nil {
return nil, errors.Wrap(err, "failed get oldest active session")
}
return &s, nil
}
func UpdateSessionLastActive(userID uint, deviceKey string, lastActive int64) error {
return errors.WithStack(db.Model(&model.Session{}).Where("user_id = ? AND device_key = ?", userID, deviceKey).Update("last_active", lastActive).Error)
}
func ListSessionsByUser(userID uint) ([]model.Session, error) {
var sessions []model.Session
err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where("user_id = ? AND status = ?", userID, model.SessionActive).Find(&sessions).Error
return sessions, errors.WithStack(err)
}
func ListSessions() ([]model.Session, error) {
var sessions []model.Session
err := db.Select("user_id, device_key, last_active, status, user_agent, ip").Where("status = ?", model.SessionActive).Find(&sessions).Error
return sessions, errors.WithStack(err)
}
func MarkInactive(sessionID string) error {
return errors.WithStack(db.Model(&model.Session{}).Where("device_key = ?", sessionID).Update("status", model.SessionInactive).Error)
}

View File

@@ -83,6 +83,14 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
return users, count, nil
}
func GetAllUsers() ([]model.User, error) {
var users []model.User
if err := db.Find(&users).Error; err != nil {
return nil, errors.WithStack(err)
}
return users, nil
}
func DeleteUserById(id uint) error {
return errors.WithStack(db.Delete(&model.User{}, id).Error)
}

138
internal/device/session.go Normal file
View File

@@ -0,0 +1,138 @@
package device
import (
"time"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
"gorm.io/gorm"
)
// Handle verifies device sessions for a user and upserts current session.
func Handle(userID uint, deviceKey, ua, ip string) error {
ttl := setting.GetInt(conf.DeviceSessionTTL, 86400)
if ttl > 0 {
_ = db.DeleteSessionsBefore(time.Now().Unix() - int64(ttl))
}
ip = utils.MaskIP(ip)
now := time.Now().Unix()
sess, err := db.GetSession(userID, deviceKey)
if err == nil {
if sess.Status == model.SessionInactive {
return errors.WithStack(errs.SessionInactive)
}
sess.Status = model.SessionActive
sess.LastActive = now
sess.UserAgent = ua
sess.IP = ip
return db.UpsertSession(sess)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
max := setting.GetInt(conf.MaxDevices, 0)
if max > 0 {
count, err := db.CountActiveSessionsByUser(userID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, err := db.GetOldestActiveSession(userID); err == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
s := &model.Session{UserID: userID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}
return db.CreateSession(s)
}
// EnsureActiveOnLogin is used only in login flow:
// - If session exists (even Inactive): reactivate and refresh fields.
// - If not exists: apply max-devices policy, then create Active session.
func EnsureActiveOnLogin(userID uint, deviceKey, ua, ip string) error {
ip = utils.MaskIP(ip)
now := time.Now().Unix()
sess, err := db.GetSession(userID, deviceKey)
if err == nil {
if sess.Status == model.SessionInactive {
max := setting.GetInt(conf.MaxDevices, 0)
if max > 0 {
count, err := db.CountActiveSessionsByUser(userID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
}
sess.Status = model.SessionActive
sess.LastActive = now
sess.UserAgent = ua
sess.IP = ip
return db.UpsertSession(sess)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
max := setting.GetInt(conf.MaxDevices, 0)
if max > 0 {
count, err := db.CountActiveSessionsByUser(userID)
if err != nil {
return err
}
if count >= int64(max) {
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
if policy == "evict_oldest" {
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
return err
}
}
} else {
return errors.WithStack(errs.TooManyDevices)
}
}
}
return db.CreateSession(&model.Session{
UserID: userID,
DeviceKey: deviceKey,
UserAgent: ua,
IP: ip,
LastActive: now,
Status: model.SessionActive,
})
}
// Refresh updates last_active for the session.
func Refresh(userID uint, deviceKey string) {
_ = db.UpdateSessionLastActive(userID, deviceKey, time.Now().Unix())
}

8
internal/errs/device.go Normal file
View File

@@ -0,0 +1,8 @@
package errs
import "errors"
var (
TooManyDevices = errors.New("too many active devices")
SessionInactive = errors.New("session inactive")
)

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ package model
import "time"
type LabelFileBinDing struct {
type LabelFileBinding struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
UserId uint `json:"user_id"` // use to user_id
LabelId uint `json:"label_id"` // use to label_id

View File

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

View File

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

16
internal/model/session.go Normal file
View File

@@ -0,0 +1,16 @@
package model
// Session represents a device session of a user.
type Session struct {
UserID uint `json:"user_id" gorm:"index"`
DeviceKey string `json:"device_key" gorm:"primaryKey;size:64"`
UserAgent string `json:"user_agent" gorm:"size:255"`
IP string `json:"ip" gorm:"size:64"`
LastActive int64 `json:"last_active"`
Status int `json:"status"`
}
const (
SessionActive = iota
SessionInactive
)

View File

@@ -145,13 +145,28 @@ func (u *User) CheckPathLimit() bool {
}
func (u *User) JoinPath(reqPath string) (string, error) {
if reqPath == "/" {
return utils.FixAndCleanPath(u.BasePath), nil
}
path, err := utils.JoinBasePath(u.BasePath, reqPath)
if err != nil {
return "", err
}
if u.CheckPathLimit() && !utils.IsSubPath(u.BasePath, path) {
return "", errs.PermissionDenied
if path != "/" && u.CheckPathLimit() {
basePaths := GetAllBasePathsFromRoles(u)
match := false
for _, base := range basePaths {
if utils.IsSubPath(base, path) {
match = true
break
}
}
if !match {
return "", errs.PermissionDenied
}
}
return path, nil
}
@@ -193,3 +208,33 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
func (u *User) WebAuthnIcon() string {
return "https://alistgo.com/logo.svg"
}
// FetchRole is used to load role details by id. It should be set by the op package
// to avoid an import cycle between model and op.
var FetchRole func(uint) (*Role, error)
// GetAllBasePathsFromRoles returns all permission paths from user's roles
func GetAllBasePathsFromRoles(u *User) []string {
basePaths := make([]string, 0)
seen := make(map[string]struct{})
for _, rid := range u.Role {
if FetchRole == nil {
continue
}
role, err := FetchRole(uint(rid))
if err != nil || role == nil {
continue
}
for _, entry := range role.PermissionScopes {
if entry.Path == "" {
continue
}
if _, ok := seen[entry.Path]; !ok {
basePaths = append(basePaths, entry.Path)
seen[entry.Path] = struct{}{}
}
}
}
return basePaths
}

View File

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

View File

@@ -23,6 +23,7 @@ type CreateLabelFileBinDingReq struct {
Type int `json:"type"`
HashInfoStr string `json:"hashinfo"`
LabelIds string `json:"label_ids"`
LabelIDs []uint64 `json:"labelIdList"`
}
type ObjLabelResp struct {
@@ -54,23 +55,29 @@ func GetLabelByFileName(userId uint, fileName string) ([]model.Label, error) {
return labels, nil
}
func GetLabelsByFileNamesPublic(fileNames []string) (map[string][]model.Label, error) {
return db.GetLabelsByFileNamesPublic(fileNames)
}
func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
if err := db.DelLabelFileBinDingByFileName(userId, req.Name); err != nil {
return errors.WithMessage(err, "failed del label_file_bin_ding in database")
}
if req.LabelIds == "" {
ids, err := collectLabelIDs(req)
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
labelMap := strings.Split(req.LabelIds, ",")
for _, value := range labelMap {
labelId, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid label ID '%s': %v", value, err)
}
if err = db.CreateLabelFileBinDing(req.Name, uint(labelId), userId); err != nil {
for _, id := range ids {
if err = db.CreateLabelFileBinDing(req.Name, uint(id), userId); err != nil {
return errors.WithMessage(err, "failed labels in database")
}
}
if !db.GetFileByNameExists(req.Name) {
objFile := model.ObjFile{
Id: req.Id,
@@ -86,8 +93,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
Type: req.Type,
HashInfoStr: req.HashInfoStr,
}
err := db.CreateObjFile(objFile)
if err != nil {
if err := db.CreateObjFile(objFile); err != nil {
return errors.WithMessage(err, "failed file in database")
}
}
@@ -97,7 +103,7 @@ func CreateLabelFileBinDing(req CreateLabelFileBinDingReq, userId uint) error {
func GetFileByLabel(userId uint, labelId string) (result []ObjLabelResp, err error) {
labelMap := strings.Split(labelId, ",")
var labelIds []uint
var labelsFile []model.LabelFileBinDing
var labelsFile []model.LabelFileBinding
var labels []model.Label
var labelsFileMap = make(map[string][]model.Label)
var labelsMap = make(map[uint]model.Label)
@@ -157,3 +163,33 @@ func StringSliceToUintSlice(strSlice []string) ([]uint, error) {
}
return uintSlice, nil
}
func RestoreLabelFileBindings(bindings []model.LabelFileBinding, keepIDs bool, override bool) error {
return db.RestoreLabelFileBindings(bindings, keepIDs, override)
}
func collectLabelIDs(req CreateLabelFileBinDingReq) ([]uint64, error) {
if len(req.LabelIDs) > 0 {
return req.LabelIDs, nil
}
s := strings.TrimSpace(req.LabelIds)
if s == "" {
return nil, nil
}
replacer := strings.NewReplacer("", ",", "、", ",", "", ",", ";", ",")
s = replacer.Replace(s)
parts := strings.Split(s, ",")
ids := make([]uint64, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
id, err := strconv.ParseUint(p, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid label ID '%s': %v", p, err)
}
ids = append(ids, id)
}
return ids, nil
}

View File

@@ -2,10 +2,11 @@ package op
import (
"fmt"
"github.com/pkg/errors"
"strconv"
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
@@ -16,6 +17,10 @@ import (
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
var roleG singleflight.Group[*model.Role]
func init() {
model.FetchRole = GetRole
}
func GetRole(id uint) (*model.Role, error) {
key := fmt.Sprint(id)
if r, ok := roleCache.Get(key); ok {
@@ -47,6 +52,23 @@ func GetRoleByName(name string) (*model.Role, error) {
return r, err
}
func GetDefaultRoleID() int {
item, err := GetSettingItemByKey(conf.DefaultRole)
if err == nil && item != nil && item.Value != "" {
if id, err := strconv.Atoi(item.Value); err == nil && id != 0 {
return id
}
if r, err := db.GetRoleByName(item.Value); err == nil {
return int(r.ID)
}
}
var r model.Role
if err := db.GetDb().Where("`default` = ?", true).First(&r).Error; err == nil {
return int(r.ID)
}
return int(model.GUEST)
}
func GetRolesByUserID(userID uint) ([]model.Role, error) {
user, err := GetUserById(userID)
if err != nil {
@@ -89,7 +111,21 @@ func CreateRole(r *model.Role) error {
}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.CreateRole(r)
if err := db.CreateRole(r); err != nil {
return err
}
if r.Default {
roleCache.Clear()
item, err := GetSettingItemByKey(conf.DefaultRole)
if err != nil {
return err
}
item.Value = strconv.Itoa(int(r.ID))
if err := SaveSettingItem(item); err != nil {
return err
}
}
return nil
}
func UpdateRole(r *model.Role) error {
@@ -106,29 +142,43 @@ func UpdateRole(r *model.Role) error {
for i := range r.PermissionScopes {
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
}
if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
oldPath := old.PermissionScopes[0].Path
newPath := r.PermissionScopes[0].Path
users, err := db.GetUsersByRole(int(r.ID))
if err != nil {
return errors.WithMessage(err, "failed to get users by role")
}
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
if err != nil {
return errors.WithMessage(err, "failed to update user base path when role updated")
}
for _, name := range modifiedUsernames {
userCache.Del(name)
}
}
//if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
// old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
//
// oldPath := old.PermissionScopes[0].Path
// newPath := r.PermissionScopes[0].Path
//
// users, err := db.GetUsersByRole(int(r.ID))
// if err != nil {
// return errors.WithMessage(err, "failed to get users by role")
// }
//
// modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
// if err != nil {
// return errors.WithMessage(err, "failed to update user base path when role updated")
// }
//
// for _, name := range modifiedUsernames {
// userCache.Del(name)
// }
//}
roleCache.Del(fmt.Sprint(r.ID))
roleCache.Del(r.Name)
return db.UpdateRole(r)
if err := db.UpdateRole(r); err != nil {
return err
}
if r.Default {
roleCache.Clear()
item, err := GetSettingItemByKey(conf.DefaultRole)
if err != nil {
return err
}
item.Value = strconv.Itoa(int(r.ID))
if err := SaveSettingItem(item); err != nil {
return err
}
}
return nil
}
func DeleteRole(id uint) error {

View File

@@ -41,15 +41,27 @@ func GetStorageByMountPath(mountPath string) (driver.Driver, error) {
return storageDriver, nil
}
func firstPathSegment(p string) string {
p = utils.FixAndCleanPath(p)
p = strings.TrimPrefix(p, "/")
if p == "" {
return ""
}
if i := strings.Index(p, "/"); i >= 0 {
return p[:i]
}
return p
}
// CreateStorage Save the storage to database so storage can get an id
// then instantiate corresponding driver and save it in memory
func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
if storage.MountPath == "/" {
return 0, errors.New("Mount path cannot be '/'")
}
//if storage.MountPath == "/" {
// return 0, errors.New("Mount path cannot be '/'")
//}
var err error
// check driver first
@@ -210,9 +222,9 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
}
storage.Modified = time.Now()
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
if storage.MountPath == "/" {
return errors.New("Mount path cannot be '/'")
}
//if storage.MountPath == "/" {
// return errors.New("Mount path cannot be '/'")
//}
err = db.UpdateStorage(&storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in database")
@@ -232,12 +244,20 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
roleCache.Del(fmt.Sprint(id))
}
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed to update user base path")
}
for _, name := range modifiedUsernames {
userCache.Del(name)
//modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldStorage.MountPath, storage.MountPath)
//if err != nil {
// return errors.WithMessage(err, "failed to update user base path")
//}
for _, id := range modifiedRoleIDs {
roleCache.Del(fmt.Sprint(id))
users, err := db.GetUsersByRole(int(id))
if err != nil {
return errors.WithMessage(err, "failed to get users by role")
}
for _, user := range users {
userCache.Del(user.Username)
}
}
}
if err != nil {
@@ -259,6 +279,34 @@ func DeleteStorageById(ctx context.Context, id uint) error {
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
firstMount := firstPathSegment(storage.MountPath)
if firstMount != "" {
roles, err := db.GetAllRoles()
if err != nil {
return errors.WithMessage(err, "failed to load roles")
}
users, err := db.GetAllUsers()
if err != nil {
return errors.WithMessage(err, "failed to load users")
}
var usedBy []string
for _, r := range roles {
for _, entry := range r.PermissionScopes {
if firstPathSegment(entry.Path) == firstMount {
usedBy = append(usedBy, "role:"+r.Name)
break
}
}
}
for _, u := range users {
if firstPathSegment(u.BasePath) == firstMount {
usedBy = append(usedBy, "user:"+u.Username)
}
}
if len(usedBy) > 0 {
return errors.Errorf("storage is used by %s, please cancel usage first", strings.Join(usedBy, ", "))
}
}
if !storage.Disabled {
storageDriver, err := GetStorageByMountPath(storage.MountPath)
if err != nil {

View File

@@ -50,6 +50,10 @@ func GetUserByRole(role int) (*model.User, error) {
return db.GetUserByRole(role)
}
func GetUsersByRole(role int) ([]model.User, error) {
return db.GetUsersByRole(role)
}
func GetUserByName(username string) (*model.User, error) {
if username == "" {
return nil, errs.EmptyUsername
@@ -124,17 +128,17 @@ func UpdateUser(u *model.User) error {
}
userCache.Del(old.Username)
u.BasePath = utils.FixAndCleanPath(u.BasePath)
if len(u.Role) > 0 {
roles, err := GetRolesByUserID(u.ID)
if err == nil {
for _, role := range roles {
if len(role.PermissionScopes) > 0 {
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
break
}
}
}
}
//if len(u.Role) > 0 {
// roles, err := GetRolesByUserID(u.ID)
// if err == nil {
// for _, role := range roles {
// if len(role.PermissionScopes) > 0 {
// u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
// break
// }
// }
// }
//}
return db.UpdateUser(u)
}

View File

@@ -0,0 +1,8 @@
package session
import "github.com/alist-org/alist/v3/internal/db"
// MarkInactive marks the session with the given ID as inactive.
func MarkInactive(sessionID string) error {
return db.MarkInactive(sessionID)
}

30
pkg/utils/mask.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import "strings"
// MaskIP anonymizes middle segments of an IP address.
func MaskIP(ip string) string {
if ip == "" {
return ""
}
if strings.Contains(ip, ":") {
parts := strings.Split(ip, ":")
if len(parts) > 2 {
for i := 1; i < len(parts)-1; i++ {
if parts[i] != "" {
parts[i] = "*"
}
}
return strings.Join(parts, ":")
}
return ip
}
parts := strings.Split(ip, ".")
if len(parts) == 4 {
for i := 1; i < len(parts)-1; i++ {
parts[i] = "*"
}
return strings.Join(parts, ".")
}
return ip
}

View File

@@ -88,6 +88,13 @@ func JoinBasePath(basePath, reqPath string) (string, error) {
strings.Contains(reqPath, "/../") {
return "", errs.RelativePath
}
reqPath = FixAndCleanPath(reqPath)
if strings.HasPrefix(reqPath, "/") {
return reqPath, nil
}
return stdpath.Join(FixAndCleanPath(basePath), FixAndCleanPath(reqPath)), nil
}

View File

@@ -43,17 +43,23 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 {
if err != nil {
continue
}
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) {
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
for _, entry := range role.PermissionScopes {
perm |= entry.Permission
}
} else {
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) {
perm |= entry.Permission
}
}
}
}
return perm
}
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
if !canReadPathByRole(u, reqPath) {
if !CanReadPathByRole(u, reqPath) {
return false
}
perm := MergeRolePermissions(u, reqPath)
@@ -78,7 +84,30 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin
return meta.Password == password
}
func canReadPathByRole(u *model.User, reqPath string) bool {
func CanReadPathByRole(u *model.User, reqPath string) bool {
if u == nil {
return false
}
if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
return len(u.Role) > 0
}
for _, rid := range u.Role {
role, err := op.GetRole(uint(rid))
if err != nil {
continue
}
for _, entry := range role.PermissionScopes {
if utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) {
return true
}
}
}
return false
}
// HasChildPermission checks whether any child path under reqPath grants the
// specified permission bit.
func HasChildPermission(u *model.User, reqPath string, bit uint) bool {
if u == nil {
return false
}
@@ -88,7 +117,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
continue
}
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) {
if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
return true
}
}
@@ -102,7 +131,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
perm := MergeRolePermissions(u, reqPath)
if HasPermission(perm, PermPathLimit) {
return canReadPathByRole(u, reqPath)
return CanReadPathByRole(u, reqPath)
}
return true
}

View File

@@ -3,14 +3,22 @@ package handles
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image/png"
"path"
"strings"
"time"
"github.com/Xhofe/go-cache"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/device"
"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/internal/session"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
@@ -79,16 +87,62 @@ func loginHash(c *gin.Context, req *LoginReq) {
return
}
}
clientID := c.GetHeader("Client-Id")
if clientID == "" {
clientID = c.Query("client_id")
}
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s",
user.ID, clientID))
if err := device.EnsureActiveOnLogin(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
if errors.Is(err, errs.TooManyDevices) {
common.ErrorResp(c, err, 403)
} else {
common.ErrorResp(c, err, 400, true)
}
return
}
// generate token
token, err := common.GenerateToken(user)
if err != nil {
common.ErrorResp(c, err, 400, true)
return
}
common.SuccessResp(c, gin.H{"token": token})
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
loginCache.Del(ip)
}
type RegisterReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Register a new user
func Register(c *gin.Context) {
if !setting.GetBool(conf.AllowRegister) {
common.ErrorStrResp(c, "registration is disabled", 403)
return
}
var req RegisterReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user := &model.User{
Username: req.Username,
Role: model.Roles{op.GetDefaultRoleID()},
Authn: "[]",
}
user.SetPassword(req.Password)
if err := op.CreateUser(user); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}
type UserResp struct {
model.User
Otp bool `json:"otp"`
@@ -111,25 +165,25 @@ func CurrentUser(c *gin.Context) {
var roleNames []string
permMap := map[string]int32{}
addedPaths := map[string]bool{}
paths := make([]string, 0)
for _, role := range user.RolesDetail {
roleNames = append(roleNames, role.Name)
for _, entry := range role.PermissionScopes {
cleanPath := path.Clean("/" + strings.TrimPrefix(entry.Path, "/"))
if _, ok := permMap[cleanPath]; !ok {
paths = append(paths, cleanPath)
}
permMap[cleanPath] |= entry.Permission
}
}
userResp.RoleNames = roleNames
for fullPath, perm := range permMap {
if !addedPaths[fullPath] {
userResp.Permissions = append(userResp.Permissions, model.PermissionEntry{
Path: fullPath,
Permission: perm,
})
addedPaths[fullPath] = true
}
for _, fullPath := range paths {
userResp.Permissions = append(userResp.Permissions, model.PermissionEntry{
Path: fullPath,
Permission: permMap[fullPath],
})
}
common.SuccessResp(c, userResp)
@@ -216,6 +270,13 @@ func Verify2FA(c *gin.Context) {
}
func LogOut(c *gin.Context) {
if keyVal, ok := c.Get("device_key"); ok {
if err := session.MarkInactive(keyVal.(string)); err != nil {
common.ErrorResp(c, err, 500)
return
}
c.Set("session_inactive", true)
}
err := common.InvalidateToken(c.GetHeader("Authorization"))
if err != nil {
common.ErrorResp(c, err, 500)

View File

@@ -107,14 +107,21 @@ func FsList(c *gin.Context) {
common.ErrorResp(c, err, 500)
return
}
total, objs := pagination(objs, &req.PageReq)
filtered := make([]model.Obj, 0, len(objs))
for _, obj := range objs {
childPath := stdpath.Join(reqPath, obj.GetName())
if common.CanReadPathByRole(user, childPath) {
filtered = append(filtered, obj)
}
}
total, objs := pagination(filtered, &req.PageReq)
provider := "unknown"
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
if err == nil {
provider = storage.GetStorage().Driver
}
common.SuccessResp(c, FsListResp{
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath), user.ID),
Content: toObjsResp(objs, reqPath, isEncrypt(meta, reqPath)),
Total: int64(total),
Readme: getReadme(meta, reqPath),
Header: getHeader(meta, reqPath),
@@ -161,7 +168,14 @@ func FsDirs(c *gin.Context) {
common.ErrorResp(c, err, 500)
return
}
dirs := filterDirs(objs)
visible := make([]model.Obj, 0, len(objs))
for _, obj := range objs {
childPath := stdpath.Join(reqPath, obj.GetName())
if common.CanReadPathByRole(user, childPath) {
visible = append(visible, obj)
}
}
dirs := filterDirs(visible)
common.SuccessResp(c, dirs)
}
@@ -224,12 +238,22 @@ func pagination(objs []model.Obj, req *model.PageReq) (int, []model.Obj) {
return total, objs[start:end]
}
func toObjsResp(objs []model.Obj, parent string, encrypt bool, userId uint) []ObjLabelResp {
func toObjsResp(objs []model.Obj, parent string, encrypt bool) []ObjLabelResp {
var resp []ObjLabelResp
names := make([]string, 0, len(objs))
for _, obj := range objs {
if !obj.IsDir() {
names = append(names, obj.GetName())
}
}
labelsByName, _ := op.GetLabelsByFileNamesPublic(names)
for _, obj := range objs {
var labels []model.Label
if obj.IsDir() == false {
labels, _ = op.GetLabelByFileName(userId, obj.GetName())
if !obj.IsDir() {
labels = labelsByName[obj.GetName()]
}
thumb, _ := model.GetThumb(obj)
resp = append(resp, ObjLabelResp{
@@ -369,7 +393,7 @@ func FsGet(c *gin.Context) {
Readme: getReadme(meta, reqPath),
Header: getHeader(meta, reqPath),
Provider: provider,
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath), user.ID),
Related: toObjsResp(related, parentPath, isEncrypt(parentMeta, parentPath)),
})
}

View File

@@ -8,7 +8,9 @@ import (
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
"net/url"
"strconv"
"strings"
)
type DelLabelFileBinDingReq struct {
@@ -16,18 +18,36 @@ type DelLabelFileBinDingReq struct {
LabelId string `json:"label_id"`
}
type pageResp[T any] struct {
Content []T `json:"content"`
Total int64 `json:"total"`
}
type restoreLabelBindingsReq struct {
KeepIDs bool `json:"keep_ids"`
Override bool `json:"override"`
Bindings []model.LabelFileBinding `json:"bindings"`
}
func GetLabelByFileName(c *gin.Context) {
fileName := c.Query("file_name")
if fileName == "" {
common.ErrorResp(c, errors.New("file_name must not empty"), 400)
return
}
decodedFileName, err := url.QueryUnescape(fileName)
if err != nil {
common.ErrorResp(c, errors.New("invalid file_name"), 400)
return
}
fmt.Println(">>> 原始 fileName:", fileName)
fmt.Println(">>> 解码后 fileName:", decodedFileName)
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
labels, err := op.GetLabelByFileName(userObj.ID, fileName)
labels, err := op.GetLabelByFileName(userObj.ID, decodedFileName)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
@@ -101,3 +121,130 @@ func GetFileByLabel(c *gin.Context) {
}
common.SuccessResp(c, fileList)
}
func ListLabelFileBinding(c *gin.Context) {
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
pageStr := c.DefaultQuery("page", "1")
sizeStr := c.DefaultQuery("page_size", "50")
page, err := strconv.Atoi(pageStr)
if err != nil || page <= 0 {
page = 1
}
pageSize, err := strconv.Atoi(sizeStr)
if err != nil || pageSize <= 0 || pageSize > 200 {
pageSize = 50
}
fileName := c.Query("file_name")
labelIDStr := c.Query("label_id")
var labelIDs []uint
if labelIDStr != "" {
parts := strings.Split(labelIDStr, ",")
for _, p := range parts {
if p == "" {
continue
}
id64, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)
if err != nil {
common.ErrorResp(c, fmt.Errorf("invalid label_id '%s': %v", p, err), 400)
return
}
labelIDs = append(labelIDs, uint(id64))
}
}
list, total, err := db.ListLabelFileBinDing(userObj.ID, labelIDs, fileName, page, pageSize)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, pageResp[model.LabelFileBinding]{
Content: list,
Total: total,
})
}
func RestoreLabelFileBinding(c *gin.Context) {
var req restoreLabelBindingsReq
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if len(req.Bindings) == 0 {
common.ErrorStrResp(c, "empty bindings", 400)
return
}
if u, ok := c.Value("user").(*model.User); ok {
for i := range req.Bindings {
if req.Bindings[i].UserId == 0 {
req.Bindings[i].UserId = u.ID
}
}
}
for i := range req.Bindings {
b := req.Bindings[i]
if b.UserId == 0 || b.LabelId == 0 || strings.TrimSpace(b.FileName) == "" {
common.ErrorStrResp(c, "invalid binding: user_id/label_id/file_name required", 400)
return
}
}
if err := op.RestoreLabelFileBindings(req.Bindings, req.KeepIDs, req.Override); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, gin.H{
"msg": fmt.Sprintf("restored %d rows", len(req.Bindings)),
})
}
func CreateLabelFileBinDingBatch(c *gin.Context) {
var req struct {
Items []op.CreateLabelFileBinDingReq `json:"items" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {
common.ErrorResp(c, err, 400)
return
}
userObj, ok := c.Value("user").(*model.User)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
type perResult struct {
Name string `json:"name"`
Ok bool `json:"ok"`
ErrMsg string `json:"errMsg,omitempty"`
}
results := make([]perResult, 0, len(req.Items))
succeed := 0
for _, item := range req.Items {
if item.IsDir {
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: "Unable to bind folder"})
continue
}
if err := op.CreateLabelFileBinDing(item, userObj.ID); err != nil {
results = append(results, perResult{Name: item.Name, Ok: false, ErrMsg: err.Error()})
continue
}
succeed++
results = append(results, perResult{Name: item.Name, Ok: true})
}
common.SuccessResp(c, gin.H{
"total": len(req.Items),
"succeed": succeed,
"failed": len(req.Items) - succeed,
"results": results,
})
}

View File

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

View File

@@ -43,28 +43,39 @@ func Search(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
nodes, total, err := search.Search(c, req.SearchReq)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
var filteredNodes []model.SearchNode
for _, node := range nodes {
if !strings.HasPrefix(node.Parent, user.BasePath) {
continue
var (
filteredNodes []model.SearchNode
)
for len(filteredNodes) < req.PerPage {
nodes, _, err := search.Search(c, req.SearchReq)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
meta, err := op.GetNearestMeta(node.Parent)
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
continue
if len(nodes) == 0 {
break
}
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
continue
for _, node := range nodes {
if !strings.HasPrefix(node.Parent, user.BasePath) {
continue
}
meta, err := op.GetNearestMeta(node.Parent)
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
continue
}
if !common.CanAccessWithRoles(user, meta, path.Join(node.Parent, node.Name), req.Password) {
continue
}
filteredNodes = append(filteredNodes, node)
if len(filteredNodes) >= req.PerPage {
break
}
}
filteredNodes = append(filteredNodes, node)
req.Page++
}
common.SuccessResp(c, common.PageResp{
Content: utils.MustSliceConvert(filteredNodes, nodeToSearchResp),
Total: total,
Total: int64(len(filteredNodes)),
})
}

92
server/handles/session.go Normal file
View File

@@ -0,0 +1,92 @@
package handles
import (
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
)
type SessionResp struct {
SessionID string `json:"session_id"`
UserID uint `json:"user_id,omitempty"`
LastActive int64 `json:"last_active"`
Status int `json:"status"`
UA string `json:"ua"`
IP string `json:"ip"`
}
func ListMySessions(c *gin.Context) {
user := c.MustGet("user").(*model.User)
sessions, err := db.ListSessionsByUser(user.ID)
if err != nil {
common.ErrorResp(c, err, 500)
return
}
resp := make([]SessionResp, len(sessions))
for i, s := range sessions {
resp[i] = SessionResp{
SessionID: s.DeviceKey,
LastActive: s.LastActive,
Status: s.Status,
UA: s.UserAgent,
IP: s.IP,
}
}
common.SuccessResp(c, resp)
}
type EvictSessionReq struct {
SessionID string `json:"session_id"`
}
func EvictMySession(c *gin.Context) {
var req EvictSessionReq
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
user := c.MustGet("user").(*model.User)
if _, err := db.GetSession(user.ID, req.SessionID); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := db.MarkInactive(req.SessionID); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}
func ListSessions(c *gin.Context) {
sessions, err := db.ListSessions()
if err != nil {
common.ErrorResp(c, err, 500)
return
}
resp := make([]SessionResp, len(sessions))
for i, s := range sessions {
resp[i] = SessionResp{
SessionID: s.DeviceKey,
UserID: s.UserID,
LastActive: s.LastActive,
Status: s.Status,
UA: s.UserAgent,
IP: s.IP,
}
}
common.SuccessResp(c, resp)
}
func EvictSession(c *gin.Context) {
var req EvictSessionReq
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := db.MarkInactive(req.SessionID); err != nil {
common.ErrorResp(c, err, 500)
return
}
common.SuccessResp(c)
}

View File

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

View File

@@ -4,6 +4,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/op"
"net/http"
"net/url"
"path"
@@ -154,7 +155,7 @@ func autoRegister(username, userID string, err error) (*model.User, error) {
Password: random.String(16),
Permission: int32(setting.GetInt(conf.SSODefaultPermission, 0)),
BasePath: setting.GetStr(conf.SSODefaultDir),
Role: nil,
Role: model.Roles{op.GetDefaultRoleID()},
Disabled: false,
SsoID: userID,
}
@@ -256,6 +257,7 @@ func OIDCLoginCallback(c *gin.Context) {
user, err = autoRegister(userID, userID, err)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
}
token, err := common.GenerateToken(user)

View File

@@ -1,9 +1,10 @@
package handles
import (
"github.com/alist-org/alist/v3/pkg/utils"
"strconv"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/server/common"
@@ -36,6 +37,9 @@ func CreateUser(c *gin.Context) {
common.ErrorResp(c, err, 400)
return
}
if len(req.Role) == 0 {
req.Role = model.Roles{op.GetDefaultRoleID()}
}
if req.IsAdmin() || req.IsGuest() {
common.ErrorStrResp(c, "admin or guest user can not be created", 400, true)
return
@@ -67,10 +71,10 @@ func UpdateUser(c *gin.Context) {
common.ErrorStrResp(c, "cannot change role of admin user", 403)
return
}
if user.Username != req.Username {
common.ErrorStrResp(c, "cannot change username of admin user", 403)
return
}
//if user.Username != req.Username {
// common.ErrorStrResp(c, "cannot change username of admin user", 403)
// return
//}
}
if req.Password == "" {
@@ -94,6 +98,14 @@ func UpdateUser(c *gin.Context) {
return
}
}
if !utils.SliceEqual(user.Role, req.Role) {
if req.IsAdmin() || req.IsGuest() {
common.ErrorStrResp(c, "cannot assign admin or guest role to user", 400, true)
return
}
}
if err := op.UpdateUser(&req); err != nil {
common.ErrorResp(c, err, 500)
} else {

View File

@@ -2,12 +2,16 @@ package middlewares
import (
"crypto/subtle"
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/device"
"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/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
@@ -24,7 +28,9 @@ func Auth(c *gin.Context) {
c.Abort()
return
}
c.Set("user", admin)
if !HandleSession(c, admin) {
return
}
log.Debugf("use admin token: %+v", admin)
c.Next()
return
@@ -50,7 +56,9 @@ func Auth(c *gin.Context) {
}
guest.RolesDetail = roles
}
c.Set("user", guest)
if !HandleSession(c, guest) {
return
}
log.Debugf("use empty token: %+v", guest)
c.Next()
return
@@ -87,11 +95,36 @@ func Auth(c *gin.Context) {
}
user.RolesDetail = roles
}
c.Set("user", user)
if !HandleSession(c, user) {
return
}
log.Debugf("use login token: %+v", user)
c.Next()
}
// HandleSession verifies device sessions and stores context values.
func HandleSession(c *gin.Context, user *model.User) bool {
clientID := c.GetHeader("Client-Id")
if clientID == "" {
clientID = c.Query("client_id")
}
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, clientID))
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
token := c.GetHeader("Authorization")
if errors.Is(err, errs.SessionInactive) {
_ = common.InvalidateToken(token)
common.ErrorResp(c, err, 401)
} else {
common.ErrorResp(c, err, 403)
}
c.Abort()
return false
}
c.Set("device_key", key)
c.Set("user", user)
return true
}
func Authn(c *gin.Context) {
token := c.GetHeader("Authorization")
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {

View File

@@ -0,0 +1,26 @@
package middlewares
import (
"github.com/alist-org/alist/v3/internal/device"
"github.com/alist-org/alist/v3/internal/model"
"github.com/gin-gonic/gin"
)
// SessionRefresh updates session's last_active after successful requests.
func SessionRefresh(c *gin.Context) {
c.Next()
if c.Writer.Status() >= 400 {
return
}
if inactive, ok := c.Get("session_inactive"); ok {
if b, ok := inactive.(bool); ok && b {
return
}
}
userVal, uok := c.Get("user")
keyVal, kok := c.Get("device_key")
if uok && kok {
user := userVal.(*model.User)
device.Refresh(user.ID, keyVal.(string))
}
}

View File

@@ -22,6 +22,7 @@ func Init(e *gin.Engine) {
})
}
Cors(e)
e.Use(middlewares.SessionRefresh)
g := e.Group(conf.URL.Path)
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
e.Use(middlewares.ForceHttps)
@@ -61,6 +62,7 @@ func Init(e *gin.Engine) {
api.POST("/auth/login", handles.Login)
api.POST("/auth/login/hash", handles.LoginHash)
api.POST("/auth/login/ldap", handles.LoginLdap)
api.POST("/auth/register", handles.Register)
auth.GET("/me", handles.CurrentUser)
auth.POST("/me/update", handles.UpdateCurrent)
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
@@ -69,6 +71,8 @@ func Init(e *gin.Engine) {
auth.POST("/auth/2fa/generate", handles.Generate2FA)
auth.POST("/auth/2fa/verify", handles.Verify2FA)
auth.GET("/auth/logout", handles.LogOut)
auth.GET("/me/sessions", handles.ListMySessions)
auth.POST("/me/sessions/evict", handles.EvictMySession)
// auth
api.GET("/auth/sso", handles.SSOLoginRedirect)
@@ -92,6 +96,8 @@ func Init(e *gin.Engine) {
_fs(auth.Group("/fs"))
_task(auth.Group("/task", middlewares.AuthNotGuest))
_label(auth.Group("/label"))
_labelFileBinding(auth.Group("/label_file_binding"))
admin(auth.Group("/admin", middlewares.AuthAdmin))
if flags.Debug || flags.Dev {
debug(g.Group("/debug"))
@@ -170,17 +176,21 @@ func admin(g *gin.RouterGroup) {
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
label := g.Group("/label")
label.GET("/list", handles.ListLabel)
label.GET("/get", handles.GetLabel)
label.POST("/create", handles.CreateLabel)
label.POST("/update", handles.UpdateLabel)
label.POST("/delete", handles.DeleteLabel)
labelFileBinding := g.Group("/label_file_binding")
labelFileBinding.GET("/get", handles.GetLabelByFileName)
labelFileBinding.GET("/get_file_by_label", handles.GetFileByLabel)
labelFileBinding.GET("/list", handles.ListLabelFileBinding)
labelFileBinding.POST("/create", handles.CreateLabelFileBinDing)
labelFileBinding.POST("/create_batch", handles.CreateLabelFileBinDingBatch)
labelFileBinding.POST("/delete", handles.DelLabelByFileName)
labelFileBinding.POST("/restore", handles.RestoreLabelFileBinding)
session := g.Group("/session")
session.GET("/list", handles.ListSessions)
session.POST("/evict", handles.EvictSession)
}
func _fs(g *gin.RouterGroup) {
@@ -216,6 +226,16 @@ func _task(g *gin.RouterGroup) {
handles.SetupTaskRoute(g)
}
func _label(g *gin.RouterGroup) {
g.GET("/list", handles.ListLabel)
g.GET("/get", handles.GetLabel)
}
func _labelFileBinding(g *gin.RouterGroup) {
g.GET("/get", handles.GetLabelByFileName)
g.GET("/get_file_by_label", handles.GetFileByLabel)
}
func Cors(r *gin.Engine) {
config := cors.DefaultConfig()
// config.AllowAllOrigins = true

View File

@@ -3,6 +3,7 @@ package server
import (
"context"
"crypto/subtle"
"fmt"
"net/http"
"net/url"
"path"
@@ -12,9 +13,11 @@ import (
"github.com/alist-org/alist/v3/server/middlewares"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/device"
"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/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/webdav"
"github.com/gin-gonic/gin"
@@ -69,6 +72,13 @@ func WebDAVAuth(c *gin.Context) {
c.Abort()
return
}
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", admin.ID, c.ClientIP()))
if err := device.Handle(admin.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
c.Status(http.StatusForbidden)
c.Abort()
return
}
c.Set("device_key", key)
c.Set("user", admin)
c.Next()
return
@@ -95,6 +105,9 @@ func WebDAVAuth(c *gin.Context) {
c.Abort()
return
}
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
user.RolesDetail = roles
}
reqPath := c.Param("path")
if reqPath == "" {
reqPath = "/"
@@ -107,7 +120,8 @@ func WebDAVAuth(c *gin.Context) {
return
}
perm := common.MergeRolePermissions(user, reqPath)
if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) {
webdavRead := common.HasPermission(perm, common.PermWebdavRead)
if user.Disabled || (!webdavRead && (c.Request.Method != "PROPFIND" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) {
if c.Request.Method == "OPTIONS" {
c.Set("user", guest)
c.Next()
@@ -142,6 +156,13 @@ func WebDAVAuth(c *gin.Context) {
c.Abort()
return
}
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, c.ClientIP()))
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
c.Status(http.StatusForbidden)
c.Abort()
return
}
c.Set("device_key", key)
c.Set("user", user)
c.Next()
}

View File

@@ -94,6 +94,7 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
depth = 0
}
meta, _ := op.GetNearestMeta(name)
user := ctx.Value("user").(*model.User)
// Read directory names.
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
@@ -108,6 +109,9 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
for _, fileInfo := range objs {
filename := path.Join(name, fileInfo.GetName())
if !common.CanReadPathByRole(user, filename) {
continue
}
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err

View File

@@ -648,6 +648,98 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
mw := multistatusWriter{w: w}
if utils.PathEqual(reqPath, user.BasePath) {
hasRootPerm := false
for _, role := range user.RolesDetail {
for _, entry := range role.PermissionScopes {
if utils.PathEqual(entry.Path, user.BasePath) {
hasRootPerm = true
break
}
}
if hasRootPerm {
break
}
}
if !hasRootPerm {
basePaths := model.GetAllBasePathsFromRoles(user)
type infoItem struct {
path string
info model.Obj
}
infos := []infoItem{{reqPath, fi}}
seen := make(map[string]struct{})
for _, p := range basePaths {
if !utils.IsSubPath(user.BasePath, p) {
continue
}
rel := strings.TrimPrefix(
strings.TrimPrefix(
utils.FixAndCleanPath(p),
utils.FixAndCleanPath(user.BasePath),
),
"/",
)
dir := strings.Split(rel, "/")[0]
if dir == "" {
continue
}
if _, ok := seen[dir]; ok {
continue
}
seen[dir] = struct{}{}
sp := utils.FixAndCleanPath(path.Join(user.BasePath, dir))
info, err := fs.Get(ctx, sp, &fs.GetArgs{})
if err != nil {
continue
}
infos = append(infos, infoItem{sp, info})
}
for _, item := range infos {
var pstats []Propstat
if pf.Propname != nil {
pnames, err := propnames(ctx, h.LockSystem, item.info)
if err != nil {
return http.StatusInternalServerError, err
}
pstat := Propstat{Status: http.StatusOK}
for _, xmlname := range pnames {
pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
}
pstats = append(pstats, pstat)
} else if pf.Allprop != nil {
pstats, err = allprop(ctx, h.LockSystem, item.info, pf.Prop)
if err != nil {
return http.StatusInternalServerError, err
}
} else {
pstats, err = props(ctx, h.LockSystem, item.info, pf.Prop)
if err != nil {
return http.StatusInternalServerError, err
}
}
rel := strings.TrimPrefix(
strings.TrimPrefix(
utils.FixAndCleanPath(item.path),
utils.FixAndCleanPath(user.BasePath),
),
"/",
)
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
if href != "/" && item.info.IsDir() {
href += "/"
}
if err := mw.write(makePropstatResponse(href, pstats)); err != nil {
return http.StatusInternalServerError, err
}
}
if err := mw.close(); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
}
walkFn := func(reqPath string, info model.Obj, err error) error {
if err != nil {
return err
@@ -671,7 +763,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
if err != nil {
return err
}
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
rel := strings.TrimPrefix(
strings.TrimPrefix(
utils.FixAndCleanPath(reqPath),
utils.FixAndCleanPath(user.BasePath),
),
"/",
)
href := utils.EncodePath(path.Join("/", h.Prefix, rel), true)
if href != "/" && info.IsDir() {
href += "/"
}
@@ -734,7 +833,7 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu
func makePropstatResponse(href string, pstats []Propstat) *response {
resp := response{
Href: []string{(&url.URL{Path: href}).EscapedPath()},
Href: []string{href},
Propstat: make([]propstat, 0, len(pstats)),
}
for _, p := range pstats {